@7365admin1/core 3.75.0 → 3.76.0
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 +19 -0
- package/dist/index.d.ts +64 -3
- package/dist/index.js +150 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +148 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/camera-live-link.test.mjs +163 -0
- package/test/default-staff-role.test.mjs +1 -1
- package/test/e2e/client-invite-service365.e2e.test.mjs +57 -0
- package/test/service-app-type.test.mjs +35 -0
package/dist/index.mjs
CHANGED
|
@@ -3175,6 +3175,20 @@ function serviceAppType(type) {
|
|
|
3175
3175
|
return type;
|
|
3176
3176
|
return APP_TYPE_ALIASES[type] ?? type;
|
|
3177
3177
|
}
|
|
3178
|
+
function storedAppTypes(type) {
|
|
3179
|
+
const app = serviceAppType(type);
|
|
3180
|
+
return [
|
|
3181
|
+
app,
|
|
3182
|
+
...Object.keys(APP_TYPE_ALIASES).filter((alias) => APP_TYPE_ALIASES[alias] === app)
|
|
3183
|
+
];
|
|
3184
|
+
}
|
|
3185
|
+
function appTypeQuery(type) {
|
|
3186
|
+
const types = storedAppTypes(type);
|
|
3187
|
+
return types.length === 1 ? types[0] : { $in: types };
|
|
3188
|
+
}
|
|
3189
|
+
function sameAppType(a, b) {
|
|
3190
|
+
return !!a && !!b && serviceAppType(a) === serviceAppType(b);
|
|
3191
|
+
}
|
|
3178
3192
|
|
|
3179
3193
|
// src/repositories/member.repo.ts
|
|
3180
3194
|
import {
|
|
@@ -3482,7 +3496,7 @@ function useMemberRepo() {
|
|
|
3482
3496
|
return cachedData;
|
|
3483
3497
|
}
|
|
3484
3498
|
try {
|
|
3485
|
-
const data = await collection.findOne({ user, type });
|
|
3499
|
+
const data = await collection.findOne({ user, type: appTypeQuery(type) });
|
|
3486
3500
|
if (!data) {
|
|
3487
3501
|
throw new NotFoundError5("Member not found.");
|
|
3488
3502
|
}
|
|
@@ -3626,7 +3640,7 @@ function useMemberRepo() {
|
|
|
3626
3640
|
search = search || "";
|
|
3627
3641
|
page = page > 0 ? page - 1 : 0;
|
|
3628
3642
|
const query2 = {
|
|
3629
|
-
type:
|
|
3643
|
+
type: type ? appTypeQuery(type) : type,
|
|
3630
3644
|
status
|
|
3631
3645
|
};
|
|
3632
3646
|
const cacheOptions = {
|
|
@@ -3755,7 +3769,7 @@ function useMemberRepo() {
|
|
|
3755
3769
|
const query2 = {
|
|
3756
3770
|
user,
|
|
3757
3771
|
org: { $nin: [null, ""] },
|
|
3758
|
-
type
|
|
3772
|
+
type: appTypeQuery(type)
|
|
3759
3773
|
};
|
|
3760
3774
|
const cacheOptions = {
|
|
3761
3775
|
page,
|
|
@@ -4172,7 +4186,7 @@ function useMemberRepo() {
|
|
|
4172
4186
|
try {
|
|
4173
4187
|
const data = await collection.findOne({
|
|
4174
4188
|
user,
|
|
4175
|
-
type,
|
|
4189
|
+
type: appTypeQuery(type),
|
|
4176
4190
|
org
|
|
4177
4191
|
});
|
|
4178
4192
|
if (!data) {
|
|
@@ -9836,7 +9850,7 @@ function pickSiteMembership(rows, type, site, org = "") {
|
|
|
9836
9850
|
if (!site)
|
|
9837
9851
|
return null;
|
|
9838
9852
|
const live = rows.filter(
|
|
9839
|
-
(m) => m.type
|
|
9853
|
+
(m) => sameAppType(m.type, type) && isLiveMembership(m) && (!org || idOf2(m.org) === org)
|
|
9840
9854
|
);
|
|
9841
9855
|
const atSite = live.find((m) => idOf2(m.siteId) === site);
|
|
9842
9856
|
if (atSite)
|
|
@@ -9850,10 +9864,10 @@ function pickLookupMembership(rows, type, site = "", org = "") {
|
|
|
9850
9864
|
const atSite = site ? pickSiteMembership(list, type, site, org) : null;
|
|
9851
9865
|
if (atSite)
|
|
9852
9866
|
return atSite;
|
|
9853
|
-
const inOrg = org ? list.find((m) => m.type
|
|
9867
|
+
const inOrg = org ? list.find((m) => sameAppType(m.type, type) && idOf2(m.org) === org.toLowerCase()) : void 0;
|
|
9854
9868
|
if (inOrg && isLiveMembership(inOrg))
|
|
9855
9869
|
return inOrg;
|
|
9856
|
-
return list.find((m) => m.type
|
|
9870
|
+
return list.find((m) => sameAppType(m.type, type)) ?? null;
|
|
9857
9871
|
}
|
|
9858
9872
|
function memberListSiteScope(memberships, org) {
|
|
9859
9873
|
const inOrg = (memberships ?? []).filter((m) => idOf2(m.org) === idOf2(org));
|
|
@@ -11827,7 +11841,7 @@ function useRoleRepo() {
|
|
|
11827
11841
|
return cachedData;
|
|
11828
11842
|
}
|
|
11829
11843
|
try {
|
|
11830
|
-
const data = await collection.find({ org, permissions: "*", type, status: "active" }).toArray();
|
|
11844
|
+
const data = await collection.find({ org, permissions: "*", type: appTypeQuery(type), status: "active" }).toArray();
|
|
11831
11845
|
setCache(cacheKey, data, 15 * 60).then(() => {
|
|
11832
11846
|
logger22.info(`Cache set for key: ${cacheKey}`);
|
|
11833
11847
|
}).catch((err) => {
|
|
@@ -11867,7 +11881,7 @@ function useRoleRepo() {
|
|
|
11867
11881
|
throw new BadRequestError33("Role name is required.");
|
|
11868
11882
|
}
|
|
11869
11883
|
type = serviceAppType(type);
|
|
11870
|
-
const query2 = type ? { name, type } : { name };
|
|
11884
|
+
const query2 = type ? { name, type: appTypeQuery(type) } : { name };
|
|
11871
11885
|
try {
|
|
11872
11886
|
const cacheKey = makeCacheKey11(namespace_collection3, query2);
|
|
11873
11887
|
const cachedData = await getCache(cacheKey);
|
|
@@ -11923,8 +11937,8 @@ function useRoleRepo() {
|
|
|
11923
11937
|
cacheOptions.search = search;
|
|
11924
11938
|
}
|
|
11925
11939
|
if (type) {
|
|
11926
|
-
query2.type =
|
|
11927
|
-
cacheOptions.type =
|
|
11940
|
+
query2.type = appTypeQuery(type);
|
|
11941
|
+
cacheOptions.type = serviceAppType(type);
|
|
11928
11942
|
}
|
|
11929
11943
|
const cacheKey = makeCacheKey11(namespace_collection3, cacheOptions);
|
|
11930
11944
|
const cachedData = await getCache(cacheKey);
|
|
@@ -14662,12 +14676,11 @@ async function ownerGrantCeiling(org, type) {
|
|
|
14662
14676
|
return null;
|
|
14663
14677
|
try {
|
|
14664
14678
|
const orgDoc = await useOrgRepo().getById(orgId).catch(() => null);
|
|
14665
|
-
|
|
14666
|
-
if (!orgDoc?.invitedRole || appType !== type)
|
|
14679
|
+
if (!orgDoc?.invitedRole || !sameAppType(orgDoc?.nature, type))
|
|
14667
14680
|
return null;
|
|
14668
14681
|
const role = await useRoleRepo().getRoleById(String(orgDoc.invitedRole)).catch(() => null);
|
|
14669
14682
|
const permissions = role?.permissions;
|
|
14670
|
-
const chosen = role && String(role.org ?? "") === orgId && role.default === true && role.type
|
|
14683
|
+
const chosen = role && String(role.org ?? "") === orgId && role.default === true && sameAppType(role.type, type) && Array.isArray(permissions) && permissions.length > 0 && !permissions.includes("*");
|
|
14671
14684
|
return chosen ? permissions.map(String) : null;
|
|
14672
14685
|
} catch {
|
|
14673
14686
|
return null;
|
|
@@ -21227,6 +21240,37 @@ function signPlaybackLink(params) {
|
|
|
21227
21240
|
expiresAt: new Date(exp * 1e3).toISOString()
|
|
21228
21241
|
};
|
|
21229
21242
|
}
|
|
21243
|
+
function signLiveLink(host, {
|
|
21244
|
+
relays = playbackRelays(),
|
|
21245
|
+
now = /* @__PURE__ */ new Date()
|
|
21246
|
+
} = {}) {
|
|
21247
|
+
const parsed = host ? parseCameraHost(host) : null;
|
|
21248
|
+
if (!parsed || parsed.channel === null)
|
|
21249
|
+
return null;
|
|
21250
|
+
const key = relays[parsed.authority];
|
|
21251
|
+
if (!key)
|
|
21252
|
+
return null;
|
|
21253
|
+
const exp = Math.floor(now.getTime() / 1e3) + PLAYBACK_LINK_SECONDS;
|
|
21254
|
+
const channel = String(parsed.channel);
|
|
21255
|
+
const sig = createHmac("sha256", key).update(`${channel}|${exp}`).digest("hex");
|
|
21256
|
+
const liveAuth = `exp=${exp}&sig=${sig}`;
|
|
21257
|
+
return {
|
|
21258
|
+
/** The player page, ready for an iframe or a WebView. */
|
|
21259
|
+
liveUrl: `https://${parsed.authority}/${channel}?${liveAuth}`,
|
|
21260
|
+
/**
|
|
21261
|
+
* The same signature, on its own, to append to any of the relay's live
|
|
21262
|
+
* routes — the socket, the still, the clip. One signature serves all four
|
|
21263
|
+
* because it covers the CHANNEL and not the path, which is what lets
|
|
21264
|
+
* `insertScreenshot` keep splicing `screenshot` into the path.
|
|
21265
|
+
*/
|
|
21266
|
+
liveAuth,
|
|
21267
|
+
liveExpiresAt: new Date(exp * 1e3).toISOString()
|
|
21268
|
+
};
|
|
21269
|
+
}
|
|
21270
|
+
function withLiveLink(fields, options) {
|
|
21271
|
+
const live = signLiveLink(fields.host, options);
|
|
21272
|
+
return live ? { ...fields, ...live } : fields;
|
|
21273
|
+
}
|
|
21230
21274
|
|
|
21231
21275
|
// src/utils/camera-capability.util.ts
|
|
21232
21276
|
var CAMERA_CAPABILITIES = [
|
|
@@ -22582,7 +22626,9 @@ function useCameraViewService() {
|
|
|
22582
22626
|
// player-page URL, which is the one picture path proven to work, and the
|
|
22583
22627
|
// plain `GET /site-cameras` already returns it to every authenticated
|
|
22584
22628
|
// caller. The credential fields are still never included.
|
|
22585
|
-
...
|
|
22629
|
+
...withLiveLink(
|
|
22630
|
+
publicCameraFields({ ...camera, siteName: site?.name })
|
|
22631
|
+
),
|
|
22586
22632
|
// Answered before anything is requested, so a camera with no address or
|
|
22587
22633
|
// an inactive one renders with a reason and costs zero device requests.
|
|
22588
22634
|
// (An ANPR unit never reaches here — it is filtered out above.)
|
|
@@ -22625,7 +22671,7 @@ function useCameraViewService() {
|
|
|
22625
22671
|
async function probeCamera(camera, cacheSeconds) {
|
|
22626
22672
|
const id = camera?._id?.toString?.() ?? "";
|
|
22627
22673
|
const identity = {
|
|
22628
|
-
...publicCameraFields(camera),
|
|
22674
|
+
...withLiveLink(publicCameraFields(camera)),
|
|
22629
22675
|
unavailableReason: snapshotRefusalReason(camera)
|
|
22630
22676
|
};
|
|
22631
22677
|
const lastFrameAt = await getCache(`lastframe:${id}`) || null;
|
|
@@ -22759,7 +22805,7 @@ function useCameraViewService() {
|
|
|
22759
22805
|
);
|
|
22760
22806
|
const { capabilities } = capabilitiesOf(camera, probe);
|
|
22761
22807
|
const base = {
|
|
22762
|
-
camera: publicCameraFields(camera),
|
|
22808
|
+
camera: withLiveLink(publicCameraFields(camera)),
|
|
22763
22809
|
snapshotSupported: refusal === null,
|
|
22764
22810
|
capabilities,
|
|
22765
22811
|
// Filled in from the probe when the device answers. Today it does not, so
|
|
@@ -25673,7 +25719,7 @@ function useOrgController() {
|
|
|
25673
25719
|
invitedModules?.permissions
|
|
25674
25720
|
);
|
|
25675
25721
|
const appOwnerRole = invitedModules?.permissions.length ? seededRoles.find(
|
|
25676
|
-
(role) => role.type
|
|
25722
|
+
(role) => sameAppType(role.type, body.nature) && role.name === "owner"
|
|
25677
25723
|
) : void 0;
|
|
25678
25724
|
if (appOwnerRole) {
|
|
25679
25725
|
try {
|
|
@@ -37968,6 +38014,87 @@ function useVehicleController() {
|
|
|
37968
38014
|
next(error);
|
|
37969
38015
|
}
|
|
37970
38016
|
}
|
|
38017
|
+
const MAX_BULK_VEHICLE_ROWS = 100;
|
|
38018
|
+
async function addBulk(req, res, next) {
|
|
38019
|
+
const rowSchema = Joi57.object({
|
|
38020
|
+
// The sheet's own row id. Opaque to the server, handed straight back.
|
|
38021
|
+
id: Joi57.string().trim().max(64).required(),
|
|
38022
|
+
block: Joi57.string().hex().length(24).required(),
|
|
38023
|
+
level: Joi57.string().hex().length(24).required(),
|
|
38024
|
+
unit: Joi57.string().hex().length(24).required(),
|
|
38025
|
+
name: Joi57.string().trim().min(1).required(),
|
|
38026
|
+
plateNumber: Joi57.string().trim().min(1).required(),
|
|
38027
|
+
nric: Joi57.string().trim().optional().allow("", null),
|
|
38028
|
+
phoneNumber: Joi57.string().trim().optional().allow("", null)
|
|
38029
|
+
});
|
|
38030
|
+
const schema2 = Joi57.object({
|
|
38031
|
+
site: Joi57.string().hex().length(24).required(),
|
|
38032
|
+
org: Joi57.string().hex().length(24).required(),
|
|
38033
|
+
category: Joi57.string().optional().valid(...Object.values(VehicleCategory)).default("resident" /* RESIDENT */),
|
|
38034
|
+
rows: Joi57.array().items(rowSchema).min(1).max(MAX_BULK_VEHICLE_ROWS).required()
|
|
38035
|
+
});
|
|
38036
|
+
const { error, value } = schema2.validate(req.body, { abortEarly: false });
|
|
38037
|
+
if (error) {
|
|
38038
|
+
const messages = error.details.map((d) => d.message).join(", ");
|
|
38039
|
+
logger79.log({ level: "error", message: messages });
|
|
38040
|
+
next(new BadRequestError102(messages));
|
|
38041
|
+
return;
|
|
38042
|
+
}
|
|
38043
|
+
try {
|
|
38044
|
+
await requireSiteReach(req, value.site);
|
|
38045
|
+
const user = req.user?.user || req.user?._id;
|
|
38046
|
+
const results = [];
|
|
38047
|
+
for (const row of value.rows) {
|
|
38048
|
+
try {
|
|
38049
|
+
const message = await _add(
|
|
38050
|
+
{
|
|
38051
|
+
site: value.site,
|
|
38052
|
+
org: value.org,
|
|
38053
|
+
type: "whitelist" /* WHITELIST */,
|
|
38054
|
+
category: value.category,
|
|
38055
|
+
name: row.name,
|
|
38056
|
+
nric: row.nric || "",
|
|
38057
|
+
phoneNumber: row.phoneNumber || "",
|
|
38058
|
+
block: row.block,
|
|
38059
|
+
level: row.level,
|
|
38060
|
+
unit: row.unit,
|
|
38061
|
+
plateNumber: [row.plateNumber]
|
|
38062
|
+
},
|
|
38063
|
+
void 0,
|
|
38064
|
+
user
|
|
38065
|
+
);
|
|
38066
|
+
results.push({
|
|
38067
|
+
id: row.id,
|
|
38068
|
+
ok: true,
|
|
38069
|
+
message: String(message ?? "Vehicle added successfully.")
|
|
38070
|
+
});
|
|
38071
|
+
} catch (rowError) {
|
|
38072
|
+
logger79.log({
|
|
38073
|
+
level: "error",
|
|
38074
|
+
message: `bulk vehicle row ${row.id}: ${rowError?.message ?? rowError}`
|
|
38075
|
+
});
|
|
38076
|
+
results.push({
|
|
38077
|
+
id: row.id,
|
|
38078
|
+
ok: false,
|
|
38079
|
+
message: String(
|
|
38080
|
+
rowError?.message ?? "The vehicle could not be added."
|
|
38081
|
+
)
|
|
38082
|
+
});
|
|
38083
|
+
}
|
|
38084
|
+
}
|
|
38085
|
+
const added = results.filter((result) => result.ok).length;
|
|
38086
|
+
const failed = results.length - added;
|
|
38087
|
+
res.status(200).json({
|
|
38088
|
+
message: failed === 0 ? `${added} vehicle${added === 1 ? "" : "s"} added.` : `${added} of ${results.length} vehicles added; ${failed} could not be added.`,
|
|
38089
|
+
added,
|
|
38090
|
+
failed,
|
|
38091
|
+
results
|
|
38092
|
+
});
|
|
38093
|
+
} catch (error2) {
|
|
38094
|
+
logger79.log({ level: "error", message: error2.message });
|
|
38095
|
+
next(error2);
|
|
38096
|
+
}
|
|
38097
|
+
}
|
|
37971
38098
|
async function add(req, res, next) {
|
|
37972
38099
|
const { error, value } = vehicleSchema.validate(req.body, {
|
|
37973
38100
|
abortEarly: false
|
|
@@ -38443,6 +38570,7 @@ function useVehicleController() {
|
|
|
38443
38570
|
}
|
|
38444
38571
|
return {
|
|
38445
38572
|
add,
|
|
38573
|
+
addBulk,
|
|
38446
38574
|
getVehicles,
|
|
38447
38575
|
getSeasonPassTypes,
|
|
38448
38576
|
getVehicleById,
|
|
@@ -91770,6 +91898,7 @@ export {
|
|
|
91770
91898
|
sessionSchema,
|
|
91771
91899
|
setIO,
|
|
91772
91900
|
shiftSchema,
|
|
91901
|
+
signLiveLink,
|
|
91773
91902
|
signPlaybackLink,
|
|
91774
91903
|
siteDayBounds,
|
|
91775
91904
|
siteModulesHandlers,
|
|
@@ -92020,6 +92149,7 @@ export {
|
|
|
92020
92149
|
vehicles_namespace_collection,
|
|
92021
92150
|
visitors_namespace_collection,
|
|
92022
92151
|
wallConfig,
|
|
92152
|
+
withLiveLink,
|
|
92023
92153
|
workOrderSchema,
|
|
92024
92154
|
work_orders2_namespace_collection,
|
|
92025
92155
|
work_orders_namespace_collection
|