@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.
- package/.changeset/attendance-site-and-identity-scope.md +32 -0
- package/.changeset/manpower-remark-site-scope.md +41 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +40 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +40 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/attendance-scope.e2e.test.mjs +420 -0
- package/test/e2e/harness.mjs +43 -1
- package/test/e2e/manpower-remarks-scope.e2e.test.mjs +504 -0
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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) {
|