@7365admin1/core 3.48.1-staging.153 → 3.48.1-staging.155
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-monitoring-site-scope.md +37 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +39 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +39 -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 +45 -1
- package/test/e2e/manpower-monitoring-scope.e2e.test.mjs +486 -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,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope the manpower monitoring settings routes to the site they belong to.
|
|
6
|
+
|
|
7
|
+
Every route on `/api/manpower-monitoring` carried `requireAuth` and nothing more,
|
|
8
|
+
so being signed in anywhere on the platform was enough to read another client's
|
|
9
|
+
shift pattern and alert email recipients, to re-time their late-check-in alert, or
|
|
10
|
+
to switch their monitoring off entirely — which silences the alert that tells a
|
|
11
|
+
client nobody turned up for a shift.
|
|
12
|
+
|
|
13
|
+
Five of the six now go through `requireSiteReach`, the rule `/api/vehicles`,
|
|
14
|
+
`/api/documents`, `/api/service-providers` and `/api/incident-reports` already
|
|
15
|
+
use:
|
|
16
|
+
|
|
17
|
+
- `POST /`, `GET /sites/:id/:serviceProviderId` and `POST /multiple` name a site
|
|
18
|
+
in the request; that site is checked before it is used. `POST /multiple`
|
|
19
|
+
upserts **by** each item's own `siteId`, so every item is checked before any of
|
|
20
|
+
them is written.
|
|
21
|
+
- `PUT /:id` names only a record, so the site is read off the **stored** row —
|
|
22
|
+
a `siteId` in the caller's own body cannot stand in for it.
|
|
23
|
+
- `GET /search` names no site at all and returned every client's row on one page.
|
|
24
|
+
It is narrowed after the read, the way the people and vehicle lookups are.
|
|
25
|
+
|
|
26
|
+
`requireSiteReach` matters here because the contracted agency that actually runs
|
|
27
|
+
these shifts holds no membership in the estate's organisation and reaches its own
|
|
28
|
+
site through `customer.sites`.
|
|
29
|
+
|
|
30
|
+
`GET /sites/active` is deliberately unchanged. It reads nothing from our database
|
|
31
|
+
— it authenticates to the external HRM Labs directory and returns that system's
|
|
32
|
+
own site tree — and the only web caller sends a hard-coded service provider id
|
|
33
|
+
rather than the signed-in one, so scoping it on that id would close the screen for
|
|
34
|
+
its real users. It needs a product decision, not a guard chosen here.
|
|
35
|
+
|
|
36
|
+
A settings row the caller cannot reach answers exactly as one that does not exist,
|
|
37
|
+
so 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>;
|
|
@@ -7893,6 +7920,7 @@ declare function useManpowerMonitoringRepo(): {
|
|
|
7893
7920
|
pageRange: string;
|
|
7894
7921
|
}>;
|
|
7895
7922
|
getManpowerSettingsBySiteId: (_id: string | ObjectId, serviceProviderId?: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
|
|
7923
|
+
getManpowerSettingsById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
|
|
7896
7924
|
updateManpowerMonitoringSettings: (_id: string | ObjectId, value: TManpowerMonitoringUpdate) => Promise<number>;
|
|
7897
7925
|
multipleManpowerMonitoringSettings: (value: TManpowerMonitoring[]) => Promise<{
|
|
7898
7926
|
updated: number;
|
|
@@ -7908,6 +7936,25 @@ declare function useManpowerMonitoringSrvc(): {
|
|
|
7908
7936
|
createManpowerMonitoringSettings: (payload: any) => Promise<mongodb.InsertOneResult<bson.Document>>;
|
|
7909
7937
|
};
|
|
7910
7938
|
|
|
7939
|
+
/**
|
|
7940
|
+
* Who may see, or change, a site's manpower monitoring settings.
|
|
7941
|
+
*
|
|
7942
|
+
* Every route on this mount carried `requireAuth` and nothing else, so being
|
|
7943
|
+
* signed in anywhere on the platform was enough to read another client's shift
|
|
7944
|
+
* pattern and alert recipients, to re-time their late-check-in alerts, or to
|
|
7945
|
+
* switch their monitoring off entirely — which silences the alert that tells a
|
|
7946
|
+
* client nobody turned up for a shift.
|
|
7947
|
+
*
|
|
7948
|
+
* The estate is the scope. Where the request names a site (`POST /`,
|
|
7949
|
+
* `GET /sites/:id/:serviceProviderId`, `POST /multiple`) that site is checked
|
|
7950
|
+
* before it is used; `PUT /:id` names only a record, so the site is read off
|
|
7951
|
+
* the STORED row and a `siteId` in the body cannot stand in for it.
|
|
7952
|
+
*
|
|
7953
|
+
* `requireSiteReach` is the rule `/api/vehicles`, `/api/documents`,
|
|
7954
|
+
* `/api/service-providers` and `/api/incident-reports` already use, so the
|
|
7955
|
+
* contracted agency that actually runs these shifts reaches its own site
|
|
7956
|
+
* through `customer.sites` exactly as it does everywhere else.
|
|
7957
|
+
*/
|
|
7911
7958
|
declare function useManpowerMonitoringCtrl(): {
|
|
7912
7959
|
createManpowerMonitoringSettings: (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
|
|
7913
7960
|
getAllManpowerSettings: (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;
|
|
@@ -75059,6 +75055,14 @@ function useManpowerMonitoringRepo() {
|
|
|
75059
75055
|
throw error;
|
|
75060
75056
|
}
|
|
75061
75057
|
}
|
|
75058
|
+
async function getManpowerSettingsById(_id) {
|
|
75059
|
+
try {
|
|
75060
|
+
_id = new import_mongodb144.ObjectId(_id);
|
|
75061
|
+
} catch (error) {
|
|
75062
|
+
throw new import_node_server_utils230.BadRequestError("Invalid ID format.");
|
|
75063
|
+
}
|
|
75064
|
+
return await collection.findOne({ _id });
|
|
75065
|
+
}
|
|
75062
75066
|
async function updateManpowerMonitoringSettings(_id, value) {
|
|
75063
75067
|
try {
|
|
75064
75068
|
_id = new import_mongodb144.ObjectId(_id);
|
|
@@ -75169,6 +75173,7 @@ function useManpowerMonitoringRepo() {
|
|
|
75169
75173
|
createManpowerMonitoringSettings,
|
|
75170
75174
|
getAllManpowerSettings,
|
|
75171
75175
|
getManpowerSettingsBySiteId,
|
|
75176
|
+
getManpowerSettingsById,
|
|
75172
75177
|
updateManpowerMonitoringSettings,
|
|
75173
75178
|
multipleManpowerMonitoringSettings,
|
|
75174
75179
|
getAllSites,
|
|
@@ -75481,6 +75486,7 @@ function useManpowerMonitoringCtrl() {
|
|
|
75481
75486
|
const {
|
|
75482
75487
|
getAllManpowerSettings: _getAllManpowerSettings,
|
|
75483
75488
|
getManpowerSettingsBySiteId: _getManpowerSettingsBySiteId,
|
|
75489
|
+
getManpowerSettingsById: _getManpowerSettingsById,
|
|
75484
75490
|
updateManpowerMonitoringSettings: _updateManpowerMonitoringSettings,
|
|
75485
75491
|
multipleManpowerMonitoringSettings: _multipleManpowerMonitoringSettings,
|
|
75486
75492
|
getAllSites: _getAllSites
|
|
@@ -75496,6 +75502,7 @@ function useManpowerMonitoringCtrl() {
|
|
|
75496
75502
|
next(new import_node_server_utils233.BadRequestError(error.message));
|
|
75497
75503
|
return;
|
|
75498
75504
|
}
|
|
75505
|
+
await requireSiteReach(req, payload.siteId);
|
|
75499
75506
|
const result = await _createManpowerMonitoringSettings(payload);
|
|
75500
75507
|
return res.json(result);
|
|
75501
75508
|
} catch (error) {
|
|
@@ -75522,7 +75529,17 @@ function useManpowerMonitoringCtrl() {
|
|
|
75522
75529
|
limit: Number(limit),
|
|
75523
75530
|
search
|
|
75524
75531
|
});
|
|
75525
|
-
|
|
75532
|
+
const { only } = await siteReachOf(req);
|
|
75533
|
+
const items = await only(
|
|
75534
|
+
(result?.items ?? []).map((row) => ({
|
|
75535
|
+
row,
|
|
75536
|
+
site: row?.siteId
|
|
75537
|
+
}))
|
|
75538
|
+
);
|
|
75539
|
+
return res.json({
|
|
75540
|
+
...result,
|
|
75541
|
+
items: items.map((entry) => entry.row)
|
|
75542
|
+
});
|
|
75526
75543
|
} catch (error) {
|
|
75527
75544
|
import_node_server_utils233.logger.log({ level: "error", message: error.message });
|
|
75528
75545
|
next(error);
|
|
@@ -75542,6 +75559,7 @@ function useManpowerMonitoringCtrl() {
|
|
|
75542
75559
|
next(new import_node_server_utils233.BadRequestError(error.message));
|
|
75543
75560
|
return;
|
|
75544
75561
|
}
|
|
75562
|
+
await requireSiteReach(req, _id);
|
|
75545
75563
|
const result = await _getManpowerSettingsBySiteId(_id, serviceProviderId);
|
|
75546
75564
|
return res.json(result);
|
|
75547
75565
|
} catch (error) {
|
|
@@ -75569,6 +75587,8 @@ function useManpowerMonitoringCtrl() {
|
|
|
75569
75587
|
next(new import_node_server_utils233.BadRequestError(error.message));
|
|
75570
75588
|
return;
|
|
75571
75589
|
}
|
|
75590
|
+
const existing = await _getManpowerSettingsById(_id);
|
|
75591
|
+
await requireSiteReach(req, existing?.siteId);
|
|
75572
75592
|
const result = await _updateManpowerMonitoringSettings(_id, payload);
|
|
75573
75593
|
return res.json(result);
|
|
75574
75594
|
} catch (error) {
|
|
@@ -75586,6 +75606,9 @@ function useManpowerMonitoringCtrl() {
|
|
|
75586
75606
|
next(new import_node_server_utils233.BadRequestError(error.message));
|
|
75587
75607
|
return;
|
|
75588
75608
|
}
|
|
75609
|
+
for (const item of payload) {
|
|
75610
|
+
await requireSiteReach(req, item?.siteId);
|
|
75611
|
+
}
|
|
75589
75612
|
const result = await _multipleManpowerMonitoringSettings(payload);
|
|
75590
75613
|
return res.json(result);
|
|
75591
75614
|
} catch (error) {
|