@7365admin1/core 3.53.0 → 3.53.2
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/CHANGELOG.md +12 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +67 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +129 -64
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/e2e/role-site-scope.e2e.test.mjs +204 -0
- package/test/file-delete-scope.test.mjs +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/core
|
|
2
2
|
|
|
3
|
+
## 3.53.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 972c701: Scope file deletion to the uploader's tenant. New uploads are stamped with the caller's user id server-side; a stamped file can only be deleted by its uploader, someone sharing an organisation with them, or a super admin. Files with no stamp (everything uploaded before this change, and the anonymous resident sign-up upload) behave exactly as before — no backfill, no migration.
|
|
8
|
+
|
|
9
|
+
## 3.53.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- bb5fb95: Scope the roles list to a site. `GET /api/roles` now accepts an optional `site`, and `role.repo.getRoles` filters on it: a role created on a site is listed only on that site, while roles with no site remain the organisation's own and stay visible on every site. `site` is part of the cache key. No stored document changes.
|
|
14
|
+
|
|
3
15
|
## 3.53.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -306,13 +306,14 @@ declare function useRoleRepo(): {
|
|
|
306
306
|
getRoleByUserId: (value: string | ObjectId) => Promise<TRole | null>;
|
|
307
307
|
getRoleById: (_id: string | ObjectId) => Promise<TRole | null>;
|
|
308
308
|
getRoleByName: (name: string, type?: string) => Promise<TRole | null>;
|
|
309
|
-
getRoles: ({ search, page, limit, sort, type, org, }: {
|
|
309
|
+
getRoles: ({ search, page, limit, sort, type, org, site, }: {
|
|
310
310
|
search?: string | undefined;
|
|
311
311
|
page?: number | undefined;
|
|
312
312
|
limit?: number | undefined;
|
|
313
313
|
sort?: any;
|
|
314
314
|
type?: string | undefined;
|
|
315
315
|
org: string | ObjectId;
|
|
316
|
+
site?: string | ObjectId | undefined;
|
|
316
317
|
}) => Promise<{}>;
|
|
317
318
|
getActiveByTypeOrg: (type: string, org: string | ObjectId) => Promise<TRole[]>;
|
|
318
319
|
updateRole: (_id: string | ObjectId, value: TMiniRole, session?: ClientSession) => Promise<ObjectId>;
|
|
@@ -643,6 +644,13 @@ type TFile = {
|
|
|
643
644
|
size?: number;
|
|
644
645
|
status?: string;
|
|
645
646
|
createdAt: string;
|
|
647
|
+
/**
|
|
648
|
+
* The signed-in user who uploaded this file, stamped server-side from the
|
|
649
|
+
* session. OPTIONAL on purpose: every row written before this shipped has no
|
|
650
|
+
* stamp, and the delete gate leaves those exactly as they were. See
|
|
651
|
+
* `utils/file-owner.util.ts`.
|
|
652
|
+
*/
|
|
653
|
+
createdBy?: string;
|
|
646
654
|
};
|
|
647
655
|
declare class MFile implements TFile {
|
|
648
656
|
_id?: ObjectId;
|
|
@@ -651,6 +659,7 @@ declare class MFile implements TFile {
|
|
|
651
659
|
size?: number;
|
|
652
660
|
status?: string;
|
|
653
661
|
createdAt: string;
|
|
662
|
+
createdBy?: string;
|
|
654
663
|
constructor(value: TFile);
|
|
655
664
|
}
|
|
656
665
|
|
|
@@ -664,7 +673,7 @@ declare function useFileRepo(): {
|
|
|
664
673
|
};
|
|
665
674
|
|
|
666
675
|
declare function useFileService(): {
|
|
667
|
-
createFile: (value: Express.Multer.File, folder?: string) => Promise<string>;
|
|
676
|
+
createFile: (value: Express.Multer.File, folder?: string, createdBy?: string) => Promise<string>;
|
|
668
677
|
deleteFile: (id: string) => Promise<string>;
|
|
669
678
|
deleteDraft: () => void;
|
|
670
679
|
};
|
package/dist/index.js
CHANGED
|
@@ -16609,7 +16609,8 @@ function useRoleRepo() {
|
|
|
16609
16609
|
limit = 10,
|
|
16610
16610
|
sort = {},
|
|
16611
16611
|
type = "",
|
|
16612
|
-
org = ""
|
|
16612
|
+
org = "",
|
|
16613
|
+
site = ""
|
|
16613
16614
|
}) {
|
|
16614
16615
|
page = page > 0 ? page - 1 : 0;
|
|
16615
16616
|
if (org && typeof org === "string" && org.length === 24) {
|
|
@@ -16619,8 +16620,19 @@ function useRoleRepo() {
|
|
|
16619
16620
|
throw new import_node_server_utils36.BadRequestError("Invalid organization ID format.");
|
|
16620
16621
|
}
|
|
16621
16622
|
}
|
|
16623
|
+
if (site && typeof site === "string" && site.length === 24) {
|
|
16624
|
+
try {
|
|
16625
|
+
site = new import_mongodb33.ObjectId(site);
|
|
16626
|
+
} catch (error) {
|
|
16627
|
+
throw new import_node_server_utils36.BadRequestError("Invalid site ID format.");
|
|
16628
|
+
}
|
|
16629
|
+
}
|
|
16622
16630
|
const query2 = { status: "active", org };
|
|
16623
16631
|
const cacheOptions = { status: "active", org };
|
|
16632
|
+
if (site) {
|
|
16633
|
+
query2.$or = [{ site }, { site: "" }, { site: null }];
|
|
16634
|
+
cacheOptions.site = site.toString();
|
|
16635
|
+
}
|
|
16624
16636
|
sort = Object.keys(sort).length > 0 ? sort : { _id: -1 };
|
|
16625
16637
|
cacheOptions.sort = JSON.stringify(sort);
|
|
16626
16638
|
if (search) {
|
|
@@ -16824,6 +16836,8 @@ var MFile = class {
|
|
|
16824
16836
|
this.size = value.size ?? 0;
|
|
16825
16837
|
this.status = value.status ?? "draft";
|
|
16826
16838
|
this.createdAt = value.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
16839
|
+
if (value.createdBy)
|
|
16840
|
+
this.createdBy = value.createdBy.toString();
|
|
16827
16841
|
}
|
|
16828
16842
|
};
|
|
16829
16843
|
|
|
@@ -19168,7 +19182,8 @@ function useRoleController() {
|
|
|
19168
19182
|
page: import_joi19.default.number().integer().min(1).allow("", null).default(1),
|
|
19169
19183
|
limit: import_joi19.default.number().integer().min(1).max(100).allow("", null).default(10),
|
|
19170
19184
|
type: import_joi19.default.string().optional().allow("", null),
|
|
19171
|
-
org: import_joi19.default.string().hex().optional().allow("", null)
|
|
19185
|
+
org: import_joi19.default.string().hex().optional().allow("", null),
|
|
19186
|
+
site: import_joi19.default.string().hex().optional().allow("", null)
|
|
19172
19187
|
});
|
|
19173
19188
|
const query2 = { ...req.query };
|
|
19174
19189
|
const { error } = validation.validate(query2);
|
|
@@ -19182,11 +19197,12 @@ function useRoleController() {
|
|
|
19182
19197
|
const limit = parseInt(req.query.limit ?? "10");
|
|
19183
19198
|
const type = req.query.type ?? "";
|
|
19184
19199
|
const org = req.query.org ?? "";
|
|
19200
|
+
const site = req.query.site ?? "";
|
|
19185
19201
|
try {
|
|
19186
19202
|
if (org) {
|
|
19187
19203
|
await requireOrgReach(req, org);
|
|
19188
19204
|
}
|
|
19189
|
-
const data = await _getRoles({ search, page, limit, type, org });
|
|
19205
|
+
const data = await _getRoles({ search, page, limit, type, org, site });
|
|
19190
19206
|
res.json(data);
|
|
19191
19207
|
return;
|
|
19192
19208
|
} catch (error2) {
|
|
@@ -22518,13 +22534,14 @@ function useFileService() {
|
|
|
22518
22534
|
bucket: SPACES_BUCKET,
|
|
22519
22535
|
forcePathStyle: true
|
|
22520
22536
|
});
|
|
22521
|
-
async function createFile(value, folder = "default") {
|
|
22537
|
+
async function createFile(value, folder = "default", createdBy) {
|
|
22522
22538
|
const session = import_node_server_utils51.useAtlas.getClient()?.startSession();
|
|
22523
22539
|
session?.startTransaction();
|
|
22524
22540
|
const file = {
|
|
22525
22541
|
name: value.originalname,
|
|
22526
22542
|
size: value.size ?? (value.buffer ? value.buffer.length : 0),
|
|
22527
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22543
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22544
|
+
...createdBy ? { createdBy } : {}
|
|
22528
22545
|
};
|
|
22529
22546
|
try {
|
|
22530
22547
|
const id = await _createFile(file, session);
|
|
@@ -27610,6 +27627,49 @@ function useVerificationController() {
|
|
|
27610
27627
|
// src/controllers/file.controller.ts
|
|
27611
27628
|
var import_node_server_utils59 = require("@7365admin1/node-server-utils");
|
|
27612
27629
|
var import_joi27 = __toESM(require("joi"));
|
|
27630
|
+
|
|
27631
|
+
// src/utils/file-owner.util.ts
|
|
27632
|
+
function fileOwnerId(file) {
|
|
27633
|
+
return file?.createdBy?.toString?.() ?? "";
|
|
27634
|
+
}
|
|
27635
|
+
function mayDeleteFile(params) {
|
|
27636
|
+
if (!params.ownerId)
|
|
27637
|
+
return true;
|
|
27638
|
+
if (!params.callerId)
|
|
27639
|
+
return false;
|
|
27640
|
+
if (params.callerId === params.ownerId)
|
|
27641
|
+
return true;
|
|
27642
|
+
if (params.callerIsSuperAdmin)
|
|
27643
|
+
return true;
|
|
27644
|
+
const ownerOrgs = new Set((params.ownerOrgIds ?? []).map(String));
|
|
27645
|
+
return (params.callerOrgIds ?? []).some((org) => ownerOrgs.has(String(org)));
|
|
27646
|
+
}
|
|
27647
|
+
|
|
27648
|
+
// src/controllers/file.controller.ts
|
|
27649
|
+
async function requireFileDelete(req, file) {
|
|
27650
|
+
const ownerId = fileOwnerId(file);
|
|
27651
|
+
if (!ownerId)
|
|
27652
|
+
return;
|
|
27653
|
+
const caller = callerId(req);
|
|
27654
|
+
if (!caller)
|
|
27655
|
+
throw new import_node_server_utils59.UnauthorizedError("Not authorized.");
|
|
27656
|
+
if (caller === ownerId)
|
|
27657
|
+
return;
|
|
27658
|
+
const [me, owner] = await Promise.all([
|
|
27659
|
+
resolveInviteActor(caller),
|
|
27660
|
+
resolveInviteActor(ownerId)
|
|
27661
|
+
]);
|
|
27662
|
+
if (mayDeleteFile({
|
|
27663
|
+
ownerId,
|
|
27664
|
+
callerId: caller,
|
|
27665
|
+
callerIsSuperAdmin: me.isSuperAdmin,
|
|
27666
|
+
callerOrgIds: me.orgIds,
|
|
27667
|
+
ownerOrgIds: owner.orgIds
|
|
27668
|
+
})) {
|
|
27669
|
+
return;
|
|
27670
|
+
}
|
|
27671
|
+
throw new import_node_server_utils59.UnauthorizedError("Not authorized.");
|
|
27672
|
+
}
|
|
27613
27673
|
function useFileController() {
|
|
27614
27674
|
const { createFile, deleteFile: _deleteFile } = useFileService();
|
|
27615
27675
|
const { getFileById: _getFileById } = useFileRepo();
|
|
@@ -27619,7 +27679,7 @@ function useFileController() {
|
|
|
27619
27679
|
return;
|
|
27620
27680
|
}
|
|
27621
27681
|
try {
|
|
27622
|
-
const id = await createFile(req.file);
|
|
27682
|
+
const id = await createFile(req.file, "default", callerId(req) || void 0);
|
|
27623
27683
|
res.status(201).json({ message: "Successfully uploaded file", id });
|
|
27624
27684
|
return;
|
|
27625
27685
|
} catch (error) {
|
|
@@ -27638,6 +27698,7 @@ function useFileController() {
|
|
|
27638
27698
|
return;
|
|
27639
27699
|
}
|
|
27640
27700
|
try {
|
|
27701
|
+
await requireFileDelete(req, await _getFileById(_id));
|
|
27641
27702
|
const message = await _deleteFile(_id);
|
|
27642
27703
|
res.json({ message });
|
|
27643
27704
|
return;
|