@7365admin1/core 3.47.1-staging.136 → 3.47.1-staging.138
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/customer-site-org-scope.md +48 -0
- package/.changeset/verification-status-transition.md +35 -0
- package/dist/index.js +45 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +66 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/customer-site-scope.e2e.test.mjs +337 -0
- package/test/e2e/harness.mjs +24 -2
- package/test/e2e/verification-status.e2e.test.mjs +159 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope `/api/customer-sites` to the organisations on the record.
|
|
6
|
+
|
|
7
|
+
Every route on this module except the pre-login invite acceptance carried
|
|
8
|
+
`requireAuth` and nothing more, and took the organisation straight out of the
|
|
9
|
+
query string or the body (`customer-site.controller.ts`). Two things followed
|
|
10
|
+
from that, and both are closed here.
|
|
11
|
+
|
|
12
|
+
`POST /api/customer-sites` is not merely an engagement record: it CREATES A SITE
|
|
13
|
+
inside whatever `siteOrg` the body names, and writes that site onto the
|
|
14
|
+
organisation as its default when it has none
|
|
15
|
+
(`customer-site.service.ts:77-111`). Any signed-in account on the platform — a
|
|
16
|
+
resident, a cleaner, a guard — could therefore plant a site inside any client.
|
|
17
|
+
|
|
18
|
+
A `customer.sites` row is also the cross-organisation access grant itself: the
|
|
19
|
+
"engaged agency" branch of `resolveSiteAccess` reads it to decide who may reach
|
|
20
|
+
a site's cameras, HID readers and residents
|
|
21
|
+
(`camera-view.service.ts:325-341`). Being able to write, edit or delete those
|
|
22
|
+
rows by id was being able to grant yourself reach into another client's estate,
|
|
23
|
+
or revoke a contractor's.
|
|
24
|
+
|
|
25
|
+
The caller is now resolved from the session and the decision made before
|
|
26
|
+
anything is read or written:
|
|
27
|
+
|
|
28
|
+
- `POST /` requires `requireOrgAccess` on the body's `org`, and on `siteOrg`
|
|
29
|
+
too when it names a different organisation — because that is where the new
|
|
30
|
+
site lands. The Add / Edit Site form sends the same id for both
|
|
31
|
+
(`web-app-org components/SiteForm.vue:397-402`).
|
|
32
|
+
- `GET /` requires `requireOrgAccess` on the `org` filter the caller sends.
|
|
33
|
+
- `GET /:id`, `PUT /:id` and `DELETE /:id` load the row first and decide from
|
|
34
|
+
the row's own `org` and `siteOrg` — Seven365 staff, or a live member of
|
|
35
|
+
EITHER side of the engagement. Either, not one: the estate's manager reaches
|
|
36
|
+
the row through `siteOrg` and the contracted agency reaches the same row
|
|
37
|
+
through `org`, so scoping to one side would break the other's screen.
|
|
38
|
+
- `GET /service-provider/:id` takes a SITE id, and uses `entitleSite` — the
|
|
39
|
+
same site-reach rule the camera, HID and people modules already use, so an
|
|
40
|
+
agency contracted to a site still lists who else works there.
|
|
41
|
+
|
|
42
|
+
`POST /invite/:id` is unchanged and still reachable with no session: an invited
|
|
43
|
+
organisation accepts before it has an account (`web-app-main pages/sign-in.vue:182`).
|
|
44
|
+
|
|
45
|
+
No new authorization mechanism is introduced. `requireOrgAccess` and
|
|
46
|
+
`entitleSite` are the helpers the member, role, people, camera and HID paths
|
|
47
|
+
already use, and the "either side" test is `resolveInviteActor` asked about two
|
|
48
|
+
organisations in one round trip rather than two.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Restrict the unauthenticated invitation-status write to the one transition its
|
|
6
|
+
callers actually perform.
|
|
7
|
+
|
|
8
|
+
`PATCH /api/auth/verification/status` (`API-core auth.route.ts:36`) carries no
|
|
9
|
+
middleware, and it cannot be given any: every caller is an invitation-landing
|
|
10
|
+
page opened from an emailed link **before an account exists** —
|
|
11
|
+
`web-app-main` `pages/verify/invitation/[id].vue`,
|
|
12
|
+
`pages/verify/service-provider/[id].vue`,
|
|
13
|
+
`pages/verify/service-provider-invite/[id].vue` and
|
|
14
|
+
`pages/sign-up-invite/service-provider/index.vue`, all through
|
|
15
|
+
`layer-common composables/useLocalAuth.ts:130`. Adding `requireAuth` would break
|
|
16
|
+
invitation acceptance for everybody, including the shipped clients.
|
|
17
|
+
|
|
18
|
+
What is restricted instead is what an anonymous caller may *do*. Each of those
|
|
19
|
+
pages calls `GET /auth/verify/:id` first, and `verify`
|
|
20
|
+
(`verification.service.ts:661-690`) already refuses an invitation that is
|
|
21
|
+
expired, cancelled or complete — so the only transition a real caller ever
|
|
22
|
+
performs is `pending -> complete`. Accepting the other three states let anyone
|
|
23
|
+
who learned an invitation id:
|
|
24
|
+
|
|
25
|
+
- set a used invitation back to `pending` and replay it,
|
|
26
|
+
- revive a `cancelled` or `expired` invitation, or
|
|
27
|
+
- burn somebody else's pending invitation by marking it `complete` before they
|
|
28
|
+
opened the email.
|
|
29
|
+
|
|
30
|
+
The handler now accepts only `complete`, loads the invitation and requires it to
|
|
31
|
+
be `pending`. Completing an already-complete invitation still answers 200 so a
|
|
32
|
+
page that calls this twice is not broken. Cancelling an invitation on purpose
|
|
33
|
+
keeps its own session-protected endpoint,
|
|
34
|
+
`PUT /api/verifications/:id/cancel` (`verification.route.ts:13`), so no
|
|
35
|
+
capability is removed.
|
package/dist/index.js
CHANGED
|
@@ -19440,7 +19440,7 @@ function useVerificationController() {
|
|
|
19440
19440
|
updateStatusById: _updateStatusById,
|
|
19441
19441
|
cancelUserInvitation: _cancelUserInvitation
|
|
19442
19442
|
} = useVerificationService();
|
|
19443
|
-
const { getVerifications: _getVerifications } = useVerificationRepo();
|
|
19443
|
+
const { getVerifications: _getVerifications, getById: _getVerificationById } = useVerificationRepo();
|
|
19444
19444
|
async function createUserInvite(req, res, next) {
|
|
19445
19445
|
const payload = { ...req.body };
|
|
19446
19446
|
const validation = import_joi21.default.object({
|
|
@@ -19825,7 +19825,7 @@ function useVerificationController() {
|
|
|
19825
19825
|
async function updateVerificationStatus(req, res, next) {
|
|
19826
19826
|
const validation = import_joi21.default.object({
|
|
19827
19827
|
id: import_joi21.default.string().hex().required(),
|
|
19828
|
-
status: import_joi21.default.string().valid("
|
|
19828
|
+
status: import_joi21.default.string().valid("complete").required()
|
|
19829
19829
|
});
|
|
19830
19830
|
const { error } = validation.validate(req.body);
|
|
19831
19831
|
if (error) {
|
|
@@ -19835,6 +19835,23 @@ function useVerificationController() {
|
|
|
19835
19835
|
}
|
|
19836
19836
|
try {
|
|
19837
19837
|
const { id, status } = req.body;
|
|
19838
|
+
const verification = await _getVerificationById(id);
|
|
19839
|
+
if (!verification) {
|
|
19840
|
+
next(new import_node_server_utils47.BadRequestError("Verification not found."));
|
|
19841
|
+
return;
|
|
19842
|
+
}
|
|
19843
|
+
if (verification.status === "complete") {
|
|
19844
|
+
res.json({ message: "Successfully updated verification status." });
|
|
19845
|
+
return;
|
|
19846
|
+
}
|
|
19847
|
+
if (verification.status !== "pending") {
|
|
19848
|
+
next(
|
|
19849
|
+
new import_node_server_utils47.BadRequestError(
|
|
19850
|
+
`Verification is ${verification.status} and cannot be completed.`
|
|
19851
|
+
)
|
|
19852
|
+
);
|
|
19853
|
+
return;
|
|
19854
|
+
}
|
|
19838
19855
|
const result = await _updateStatusById(id, status);
|
|
19839
19856
|
res.json({ message: result });
|
|
19840
19857
|
return;
|
|
@@ -39590,6 +39607,15 @@ function useCustomerSiteService() {
|
|
|
39590
39607
|
// src/controllers/customer-site.controller.ts
|
|
39591
39608
|
var import_joi58 = __toESM(require("joi"));
|
|
39592
39609
|
var import_node_server_utils116 = require("@7365admin1/node-server-utils");
|
|
39610
|
+
async function requireEitherOrg(req, ...orgs) {
|
|
39611
|
+
const wanted = orgs.map((org) => org?.toString?.() ?? "").filter((org) => Boolean(org));
|
|
39612
|
+
const actor = await resolveInviteActor(callerId(req));
|
|
39613
|
+
if (actor.isSuperAdmin)
|
|
39614
|
+
return;
|
|
39615
|
+
if (wanted.length === 0 || !wanted.some((org) => actor.orgIds.includes(org))) {
|
|
39616
|
+
throw new import_node_server_utils116.UnauthorizedError("Not authorized.");
|
|
39617
|
+
}
|
|
39618
|
+
}
|
|
39593
39619
|
function useCustomerSiteController() {
|
|
39594
39620
|
const {
|
|
39595
39621
|
getAll: _getAll,
|
|
@@ -39607,6 +39633,10 @@ function useCustomerSiteController() {
|
|
|
39607
39633
|
return;
|
|
39608
39634
|
}
|
|
39609
39635
|
try {
|
|
39636
|
+
await requireOrgAccess(req, value.org);
|
|
39637
|
+
if (value.siteOrg && value.siteOrg.toString() !== value.org?.toString()) {
|
|
39638
|
+
await requireOrgAccess(req, value.siteOrg);
|
|
39639
|
+
}
|
|
39610
39640
|
const data = await _add(value);
|
|
39611
39641
|
res.status(201).json(data);
|
|
39612
39642
|
return;
|
|
@@ -39674,6 +39704,7 @@ function useCustomerSiteController() {
|
|
|
39674
39704
|
}
|
|
39675
39705
|
});
|
|
39676
39706
|
try {
|
|
39707
|
+
await requireOrgAccess(req, org);
|
|
39677
39708
|
const data = await _getAll({
|
|
39678
39709
|
search,
|
|
39679
39710
|
page,
|
|
@@ -39705,6 +39736,13 @@ function useCustomerSiteController() {
|
|
|
39705
39736
|
return;
|
|
39706
39737
|
}
|
|
39707
39738
|
try {
|
|
39739
|
+
const actor = await resolveInviteActor(callerId(req));
|
|
39740
|
+
if (!actor.isSuperAdmin) {
|
|
39741
|
+
await useCameraViewService().entitleSite({
|
|
39742
|
+
siteId: site,
|
|
39743
|
+
userId: actor.id
|
|
39744
|
+
});
|
|
39745
|
+
}
|
|
39708
39746
|
const data = await _getBySiteAsServiceProvider(site);
|
|
39709
39747
|
res.json(data);
|
|
39710
39748
|
return;
|
|
@@ -39726,6 +39764,7 @@ function useCustomerSiteController() {
|
|
|
39726
39764
|
}
|
|
39727
39765
|
try {
|
|
39728
39766
|
const data = await _getById(id);
|
|
39767
|
+
await requireEitherOrg(req, data?.org, data?.siteOrg);
|
|
39729
39768
|
res.json(data);
|
|
39730
39769
|
return;
|
|
39731
39770
|
} catch (error2) {
|
|
@@ -39744,6 +39783,8 @@ function useCustomerSiteController() {
|
|
|
39744
39783
|
return;
|
|
39745
39784
|
}
|
|
39746
39785
|
try {
|
|
39786
|
+
const existing = await _getById(id);
|
|
39787
|
+
await requireEitherOrg(req, existing?.org, existing?.siteOrg);
|
|
39747
39788
|
const data = await _updateCusSiteById(id, req.body);
|
|
39748
39789
|
res.json(data);
|
|
39749
39790
|
return;
|
|
@@ -39755,6 +39796,8 @@ function useCustomerSiteController() {
|
|
|
39755
39796
|
async function deleteById(req, res, next) {
|
|
39756
39797
|
const id = req.params.id;
|
|
39757
39798
|
try {
|
|
39799
|
+
const existing = await _getById(id);
|
|
39800
|
+
await requireEitherOrg(req, existing?.org, existing?.siteOrg);
|
|
39758
39801
|
const data = await _deleteById(id);
|
|
39759
39802
|
res.json(data);
|
|
39760
39803
|
return;
|