@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/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: serviceAppType(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 === type && isLiveMembership(m) && (!org || idOf2(m.org) === org)
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 === type && idOf2(m.org) === org.toLowerCase()) : void 0;
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 === type) ?? null;
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 = serviceAppType(type);
11927
- cacheOptions.type = query2.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
- const appType = serviceAppType(orgDoc?.nature);
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 === appType && Array.isArray(permissions) && permissions.length > 0 && !permissions.includes("*");
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
- ...publicCameraFields({ ...camera, siteName: site?.name }),
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 === serviceAppType(body.nature) && role.name === "owner"
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