@7365admin1/core 3.48.1-staging.154 → 3.48.1-staging.156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Take the attendance caller's identity from the session, and scope every attendance
6
+ route to its estate.
7
+
8
+ Two separate defects, both on every route of `/api/attendances`.
9
+
10
+ **The caller's identity came from a cookie the caller writes.** Every handler read
11
+ `user` out of the raw `Cookie` header. That cookie is set by the browser itself
12
+ (`layer-common/composables/useLocalAuth.ts`, `setSession`) and by the mobile
13
+ clients from their own stored id; the server never compared it to the session.
14
+ `requireAuth` validates `sid` against Redis and puts the real caller on `req.user`
15
+ — which this controller ignored. A signed-in account could send
16
+ `Cookie: user=<somebody else>` and clock that person in, read their shift history
17
+ and check-in photo, or clock them out; and because check-out `$set`s `user` from
18
+ the request, checking somebody out also re-attributed their shift. The identity
19
+ now comes from `callerId(req)`, as the rest of the codebase resolves it.
20
+
21
+ **The estate was never checked.** All five routes now go through
22
+ `requireSiteReach`, the rule `/api/vehicles`, `/api/documents`,
23
+ `/api/service-providers` and `/api/incident-reports` already use. The four that
24
+ name a site in the URL check it before using it; `GET /id/:id` and
25
+ `PUT /id/:id/check-out` name only a record, so the site is read off the **stored**
26
+ row. The `customer.sites` branch matters here — the contracted agency whose staff
27
+ actually work these shifts holds no membership in the estate's organisation.
28
+
29
+ Every existing client already sends its own id in that cookie, so nothing
30
+ legitimate changes. A bare `Authorization: Bearer` call that sends no cookie at
31
+ all used to fail Joi's `user is required` with a 400 and now succeeds, which is
32
+ the only behaviour that gets looser rather than tighter.
@@ -0,0 +1,41 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Scope the manpower remarks routes to the site the remarks belong to.
6
+
7
+ A remarks row is the shift-attendance exception record for one site on one day:
8
+ which named guards were late or absent, and whether the estate has acknowledged
9
+ it. Every route on `/api/remarks` carried `requireAuth` and nothing more — the
10
+ controller held no authorization identifier of any kind — so being signed in
11
+ anywhere on the platform was enough to read another client's daily manning
12
+ failures, to write an acknowledgement into their record in a name of the
13
+ caller's choosing, or to switch a row to `inactive` so the estate's own console
14
+ stops listing it.
15
+
16
+ All five routes now go through `requireSiteReach`, the rule
17
+ `/api/manpower-monitoring`, `/api/attendances`, `/api/vehicles`,
18
+ `/api/documents` and `/api/incident-reports` already use:
19
+
20
+ - `GET /:id/:serviceProviderId` names the site in the URL, so it is checked
21
+ before the read.
22
+ - `PUT /:id` and `PATCH /:id` name only a record, so the site is read off the
23
+ **stored** row — a `siteId` in the caller's own body cannot stand in for it.
24
+ - `GET /` names no site at all. Its only filter is a `serviceProviderId` the
25
+ caller supplies, which is not a claim about the caller, and one agency works
26
+ for several clients — so the page came back holding every one of those
27
+ clients' rows. It is narrowed after the read, the way
28
+ `/api/manpower-monitoring`'s own `GET /search` is. The count in the envelope
29
+ stays the unfiltered one, as it does there; both callers read it only for the
30
+ pager.
31
+ - `POST /` writes a row for whatever `siteId` the body carries. It has no caller
32
+ in any web app or mobile app — the rows are written internally by
33
+ `manpower-monitoring.service.ts` when a site's monitoring settings are created
34
+ — and it is guarded all the same rather than left open.
35
+
36
+ `requireSiteReach` matters here because the contracted agency that actually runs
37
+ these shifts holds no membership in the estate's organisation and reaches its
38
+ own site through `customer.sites`.
39
+
40
+ A row the caller cannot reach answers exactly as one that does not exist, so
41
+ site ids cannot be enumerated by watching the difference.
package/dist/index.d.ts CHANGED
@@ -4144,6 +4144,33 @@ declare function useAttendanceRepository(): {
4144
4144
  deleteAttendance: (_id: string | ObjectId, session?: ClientSession) => Promise<number>;
4145
4145
  };
4146
4146
 
4147
+ /**
4148
+ * Whose attendance this is, and which estate it was worked at.
4149
+ *
4150
+ * Two separate holes, both on every route of this mount.
4151
+ *
4152
+ * **1. The caller's identity came from a cookie the caller writes.** Every
4153
+ * handler here read `user` out of the raw `Cookie` header. That cookie is set by
4154
+ * the browser itself (`layer-common/composables/useLocalAuth.ts`, `setSession`)
4155
+ * and by the mobile clients from their own stored id; the server never checked
4156
+ * it against the session. `requireAuth` validates `sid` against Redis and puts
4157
+ * the real caller on `req.user` — which this controller ignored. So a signed-in
4158
+ * account could send `Cookie: user=<somebody else>` and check IN as them, read
4159
+ * THEIR shift history, or check them out. `checkOutAttendance` also `$set`s
4160
+ * `user` from the request, so checking somebody else out re-attributed their
4161
+ * shift to whoever asked.
4162
+ *
4163
+ * The identity now comes from `callerId(req)`, the session, exactly as the rest
4164
+ * of the codebase resolves it. Every honest client already sends its own id in
4165
+ * that cookie, so nothing legitimate changes.
4166
+ *
4167
+ * **2. The estate was never checked.** Four of the five name a site in the URL
4168
+ * and used it without asking; `GET /id/:id` and `PUT /id/:id/check-out` name only
4169
+ * a record, so the site is read off the STORED row. `requireSiteReach` is the
4170
+ * rule `/api/vehicles`, `/api/documents`, `/api/service-providers` and
4171
+ * `/api/incident-reports` already use, so the contracted agency whose staff
4172
+ * actually work these shifts reaches its own site through `customer.sites`.
4173
+ */
4147
4174
  declare function useAttendanceController(): {
4148
4175
  checkInAttendance: (req: Request, res: Response, next: NextFunction) => Promise<void>;
4149
4176
  getAllAttendances: (req: Request, res: Response, next: NextFunction) => Promise<void>;
@@ -8342,11 +8369,45 @@ declare function useManpowerRemarksRepo(): {
8342
8369
  pageRange: string;
8343
8370
  }>;
8344
8371
  getManpowerRemarksBySiteId: (_id: string | ObjectId, date: string, serviceProviderId?: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
8372
+ getManpowerRemarksById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
8345
8373
  updateManpowerRemarks: (_id: string | ObjectId, value: TManpowerRemarksUpdate) => Promise<number>;
8346
8374
  updateRemarksStatus: (_id: string | ObjectId, value: TManpowerRemarksStatusUpdate) => Promise<number>;
8347
8375
  createIndexes: () => Promise<string>;
8348
8376
  };
8349
8377
 
8378
+ /**
8379
+ * Who may see, or change, a site's manpower remarks.
8380
+ *
8381
+ * A remarks row is the shift-attendance exception record for one site on one
8382
+ * day: which named guards were late or absent, and whether the estate has
8383
+ * acknowledged it. Every route on this mount carried `requireAuth` and NOTHING
8384
+ * else — this controller held no authorization identifier of any kind — so
8385
+ * being signed in anywhere on the platform was enough to read another client's
8386
+ * daily manning failures, to write an acknowledgement into their record in
8387
+ * somebody else's name, or to switch a row to `inactive` so the estate stops
8388
+ * seeing it.
8389
+ *
8390
+ * The estate is the scope, the same rule `/api/manpower-monitoring`,
8391
+ * `/api/attendances`, `/api/vehicles`, `/api/documents` and
8392
+ * `/api/incident-reports` already use, so the contracted agency that actually
8393
+ * runs these shifts reaches its own sites through `customer.sites` and needs no
8394
+ * membership in the estate's own organisation.
8395
+ *
8396
+ * Three shapes, because the routes ask three different questions:
8397
+ * - `GET /:id/:serviceProviderId` names the site in the URL, so it is checked
8398
+ * BEFORE the read.
8399
+ * - `PUT /:id` and `PATCH /:id` name only a record, so the site is read off
8400
+ * the STORED row (`requireRemarkReach`) and a `siteId` in the body cannot
8401
+ * stand in for it.
8402
+ * - `GET /` names no site at all — its only filter is a `serviceProviderId`
8403
+ * the caller supplies — so the page has to be narrowed AFTER the read, the
8404
+ * way `/api/manpower-monitoring`'s own `GET /search` is.
8405
+ *
8406
+ * `POST /` writes a row for whatever `siteId` the body carries. It has no
8407
+ * caller in any web app or mobile app; the rows are written internally by
8408
+ * `manpower-monitoring.service.ts` when a site's monitoring settings are
8409
+ * created. It is guarded all the same rather than left open.
8410
+ */
8350
8411
  declare function useManpowerRemarkCtrl(): {
8351
8412
  createManpowerRemark: (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
8352
8413
  getManpowerRemarksAllSite: (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
package/dist/index.js CHANGED
@@ -40904,6 +40904,7 @@ function useAttendanceController() {
40904
40904
  getAllAttendances: _getAllAttendances,
40905
40905
  getAttendanceByUser: _getAttendanceByUser,
40906
40906
  getAttendanceById: _getAttendanceById,
40907
+ getRawAttendanceById: _getRawAttendanceById,
40907
40908
  deleteAttendance: _deleteAttendance
40908
40909
  } = useAttendanceRepository();
40909
40910
  const {
@@ -40911,11 +40912,7 @@ function useAttendanceController() {
40911
40912
  checkOutAttendance: _checkOutAttendance
40912
40913
  } = useAttendanceService();
40913
40914
  async function checkInAttendance(req, res, next) {
40914
- const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
40915
- (acc, [key, value]) => ({ ...acc, [key]: value }),
40916
- {}
40917
- ) : {};
40918
- const user = cookies["user"] || "";
40915
+ const user = callerId(req);
40919
40916
  const payload = { ...req.body, ...req.params, user };
40920
40917
  const { error } = attendanceSchema.validate(payload);
40921
40918
  if (error) {
@@ -40924,6 +40921,7 @@ function useAttendanceController() {
40924
40921
  return;
40925
40922
  }
40926
40923
  try {
40924
+ await requireSiteReach(req, req.params.site);
40927
40925
  const id = await _checkInAttendance(payload);
40928
40926
  res.status(201).json({ message: "Attendance created successfully.", id });
40929
40927
  return;
@@ -40958,6 +40956,7 @@ function useAttendanceController() {
40958
40956
  const startDate = req.query.dateFrom ?? "";
40959
40957
  const endDate = req.query.dateTo ?? "";
40960
40958
  try {
40959
+ await requireSiteReach(req, site);
40961
40960
  const data = await _getAllAttendances({
40962
40961
  page,
40963
40962
  limit,
@@ -40976,11 +40975,7 @@ function useAttendanceController() {
40976
40975
  }
40977
40976
  }
40978
40977
  async function getAttendanceByUser(req, res, next) {
40979
- const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
40980
- (acc, [key, value]) => ({ ...acc, [key]: value }),
40981
- {}
40982
- ) : {};
40983
- const user = cookies["user"] || "";
40978
+ const user = callerId(req);
40984
40979
  const query2 = { ...req.query, ...req.params, user };
40985
40980
  const validation = import_joi62.default.object({
40986
40981
  page: import_joi62.default.number().min(1).optional().allow("", null),
@@ -41006,6 +41001,7 @@ function useAttendanceController() {
41006
41001
  const startDate = req.query.dateFrom ?? "";
41007
41002
  const endDate = req.query.dateTo ?? "";
41008
41003
  try {
41004
+ await requireSiteReach(req, site);
41009
41005
  const data = await _getAttendanceByUser({
41010
41006
  page,
41011
41007
  limit,
@@ -41034,6 +41030,8 @@ function useAttendanceController() {
41034
41030
  return;
41035
41031
  }
41036
41032
  try {
41033
+ const existing = await _getRawAttendanceById(_id);
41034
+ await requireSiteReach(req, existing?.site);
41037
41035
  const data = await _getAttendanceById(_id);
41038
41036
  res.json(data);
41039
41037
  return;
@@ -41044,11 +41042,7 @@ function useAttendanceController() {
41044
41042
  }
41045
41043
  }
41046
41044
  async function checkOutAttendance(req, res, next) {
41047
- const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
41048
- (acc, [key, value]) => ({ ...acc, [key]: value }),
41049
- {}
41050
- ) : {};
41051
- const user = cookies["user"] || "";
41045
+ const user = callerId(req);
41052
41046
  const payload = { id: req.params.id, ...req.body, user };
41053
41047
  const validation = import_joi62.default.object({
41054
41048
  id: import_joi62.default.string().hex().required(),
@@ -41070,6 +41064,8 @@ function useAttendanceController() {
41070
41064
  }
41071
41065
  try {
41072
41066
  const { id, ...value } = payload;
41067
+ const existing = await _getRawAttendanceById(id);
41068
+ await requireSiteReach(req, existing?.site);
41073
41069
  await _checkOutAttendance(id, value);
41074
41070
  res.json({ message: "Attendance updated successfully." });
41075
41071
  return;
@@ -75339,6 +75335,14 @@ function useManpowerRemarksRepo() {
75339
75335
  throw error;
75340
75336
  }
75341
75337
  }
75338
+ async function getManpowerRemarksById(_id) {
75339
+ try {
75340
+ _id = new import_mongodb146.ObjectId(_id);
75341
+ } catch (error) {
75342
+ throw new import_node_server_utils231.BadRequestError("Invalid ID format.");
75343
+ }
75344
+ return await collection.findOne({ _id });
75345
+ }
75342
75346
  async function updateManpowerRemarks(_id, value) {
75343
75347
  try {
75344
75348
  _id = new import_mongodb146.ObjectId(_id);
@@ -75386,6 +75390,7 @@ function useManpowerRemarksRepo() {
75386
75390
  createManpowerRemarks,
75387
75391
  getManpowerRemarksAllSite,
75388
75392
  getManpowerRemarksBySiteId,
75393
+ getManpowerRemarksById,
75389
75394
  updateManpowerRemarks,
75390
75395
  updateRemarksStatus,
75391
75396
  createIndexes
@@ -77353,8 +77358,13 @@ function useManpowerRemarkCtrl() {
77353
77358
  getManpowerRemarksAllSite: _getManpowerRemarksAllSite,
77354
77359
  updateRemarksStatus: _updateRemarksStatus,
77355
77360
  getManpowerRemarksBySiteId: _getManpowerRemarksBySiteId,
77361
+ getManpowerRemarksById: _getManpowerRemarksById,
77356
77362
  updateManpowerRemarks: _updateManpowerRemarks
77357
77363
  } = useManpowerRemarksRepo();
77364
+ async function requireRemarkReach(req, _id) {
77365
+ const remark = await _getManpowerRemarksById(_id);
77366
+ await requireSiteReach(req, remark?.siteId);
77367
+ }
77358
77368
  async function createManpowerRemark(req, res, next) {
77359
77369
  try {
77360
77370
  const payload = { ...req.body };
@@ -77363,6 +77373,7 @@ function useManpowerRemarkCtrl() {
77363
77373
  next(new import_node_server_utils244.BadRequestError(error.message));
77364
77374
  return;
77365
77375
  }
77376
+ await requireSiteReach(req, payload.siteId);
77366
77377
  const result = await _createManpowerRemarks(payload);
77367
77378
  return res.json(result);
77368
77379
  } catch (error) {
@@ -77402,7 +77413,17 @@ function useManpowerRemarkCtrl() {
77402
77413
  status: status?.toString() || "",
77403
77414
  date: date?.toString() || ""
77404
77415
  });
77405
- return res.json(result);
77416
+ const { only } = await siteReachOf(req);
77417
+ const items = await only(
77418
+ (result?.items ?? []).map((row) => ({
77419
+ row,
77420
+ site: row?.siteId
77421
+ }))
77422
+ );
77423
+ return res.json({
77424
+ ...result,
77425
+ items: items.map((entry) => entry.row)
77426
+ });
77406
77427
  } catch (error) {
77407
77428
  import_node_server_utils244.logger.log({ level: "error", message: error.message });
77408
77429
  next(error);
@@ -77427,6 +77448,7 @@ function useManpowerRemarkCtrl() {
77427
77448
  next(new import_node_server_utils244.BadRequestError(error.message));
77428
77449
  return;
77429
77450
  }
77451
+ await requireSiteReach(req, id);
77430
77452
  const result = await _getManpowerRemarksBySiteId(
77431
77453
  id,
77432
77454
  date,
@@ -77454,6 +77476,7 @@ function useManpowerRemarkCtrl() {
77454
77476
  next(new import_node_server_utils244.BadRequestError(error.message));
77455
77477
  return;
77456
77478
  }
77479
+ await requireRemarkReach(req, _id);
77457
77480
  const result = await _updateManpowerRemarks(_id, payload);
77458
77481
  return res.json(result);
77459
77482
  } catch (error) {
@@ -77475,6 +77498,7 @@ function useManpowerRemarkCtrl() {
77475
77498
  next(new import_node_server_utils244.BadRequestError(error.message));
77476
77499
  return;
77477
77500
  }
77501
+ await requireRemarkReach(req, _id);
77478
77502
  const result = await _updateRemarksStatus(_id, payload);
77479
77503
  return res.json(result);
77480
77504
  } catch (error) {