@astram/identity 0.3.1 → 0.5.0
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/dist/company-membership.object.d.ts.map +1 -1
- package/dist/company-membership.object.js +3 -2
- package/dist/company.object.d.ts.map +1 -1
- package/dist/company.object.js +8 -3
- package/dist/features/access/index.d.ts.map +1 -1
- package/dist/features/access/index.js +1 -5
- package/dist/features/audit/audit-read.service.d.ts.map +1 -1
- package/dist/features/audit/audit-read.service.js +13 -6
- package/dist/features/audit/audit.controller.d.ts.map +1 -1
- package/dist/features/audit/audit.controller.js +2 -0
- package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -1
- package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -1
- package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -1
- package/dist/features/authz/catalog-reconcile.job.js +1 -3
- package/dist/features/authz/db-permission-facts.adapter.d.ts +24 -4
- package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
- package/dist/features/authz/db-permission-facts.adapter.js +92 -5
- package/dist/features/authz/grant-admin.controller.d.ts +8 -5
- package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.controller.js +22 -15
- package/dist/features/authz/grant-admin.service.d.ts +57 -11
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +163 -85
- package/dist/features/authz/grant-ceiling.d.ts +111 -0
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -0
- package/dist/features/authz/grant-ceiling.js +248 -0
- package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -1
- package/dist/features/authz/mariadb-catalog-store.js +15 -3
- package/dist/features/authz/permission-version.bumper.d.ts +45 -1
- package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
- package/dist/features/authz/permission-version.bumper.js +106 -16
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/role-binding.d.ts +36 -0
- package/dist/features/authz/role-binding.d.ts.map +1 -0
- package/dist/features/authz/role-binding.js +53 -0
- package/dist/features/company/company-hierarchy.service.d.ts +46 -0
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -0
- package/dist/features/company/company-hierarchy.service.js +100 -0
- package/dist/features/company/company.controller.d.ts +23 -5
- package/dist/features/company/company.controller.d.ts.map +1 -1
- package/dist/features/company/company.controller.js +47 -12
- package/dist/features/company/company.service.d.ts +28 -12
- package/dist/features/company/company.service.d.ts.map +1 -1
- package/dist/features/company/company.service.js +61 -14
- package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -1
- package/dist/features/cross-tenant/cross-tenant-grant.controller.js +2 -0
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +123 -6
- package/dist/features/membership/membership.controller.d.ts +6 -5
- package/dist/features/membership/membership.controller.d.ts.map +1 -1
- package/dist/features/membership/membership.controller.js +34 -22
- package/dist/features/membership/membership.service.d.ts +20 -14
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +62 -29
- package/dist/features/membership/posting-version-bump.d.ts +5 -0
- package/dist/features/membership/posting-version-bump.d.ts.map +1 -1
- package/dist/features/membership/posting.service.d.ts +20 -24
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +101 -64
- package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.controller.js +2 -0
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.service.js +4 -1
- package/dist/features/policy/policy.controller.d.ts.map +1 -1
- package/dist/features/policy/policy.controller.js +2 -0
- package/dist/features/role/role.controller.d.ts +3 -2
- package/dist/features/role/role.controller.d.ts.map +1 -1
- package/dist/features/role/role.controller.js +15 -10
- package/dist/features/role/role.service.d.ts +16 -8
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +24 -10
- package/dist/features/scope/scope-closure.service.d.ts +44 -4
- package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
- package/dist/features/scope/scope-closure.service.js +215 -82
- package/dist/features/service-account/service-account.controller.d.ts +63 -0
- package/dist/features/service-account/service-account.controller.d.ts.map +1 -0
- package/dist/features/service-account/service-account.controller.js +295 -0
- package/dist/features/service-account/service-account.service.d.ts +209 -0
- package/dist/features/service-account/service-account.service.d.ts.map +1 -0
- package/dist/features/service-account/service-account.service.js +528 -0
- package/dist/features/service-account/service-account.store.d.ts +91 -0
- package/dist/features/service-account/service-account.store.d.ts.map +1 -0
- package/dist/features/service-account/service-account.store.js +158 -0
- package/dist/features/share/share.controller.d.ts.map +1 -1
- package/dist/features/share/share.controller.js +2 -0
- package/dist/features/share/share.service.d.ts.map +1 -1
- package/dist/features/shared/identity-error.interceptor.d.ts +24 -0
- package/dist/features/shared/identity-error.interceptor.d.ts.map +1 -0
- package/dist/features/shared/identity-error.interceptor.js +82 -0
- package/dist/features/shared/identity-errors.d.ts +23 -0
- package/dist/features/shared/identity-errors.d.ts.map +1 -0
- package/dist/features/shared/identity-errors.js +41 -0
- package/dist/features/user/session-revocation-check.d.ts +11 -0
- package/dist/features/user/session-revocation-check.d.ts.map +1 -0
- package/dist/features/user/session-revocation-check.js +21 -0
- package/dist/features/user/user.controller.d.ts +11 -6
- package/dist/features/user/user.controller.d.ts.map +1 -1
- package/dist/features/user/user.controller.js +35 -17
- package/dist/features/user/user.service.d.ts +55 -14
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +143 -16
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/membership-posting.object.d.ts.map +1 -1
- package/dist/membership-posting.object.js +4 -2
- package/dist/objects.d.ts.map +1 -1
- package/dist/objects.js +14 -3
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +3 -0
- package/dist/service-account.object.d.ts +31 -0
- package/dist/service-account.object.d.ts.map +1 -0
- package/dist/service-account.object.js +75 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +3 -0
- package/migrations/1710000000018-CreateServiceAccount.cjs +53 -0
- package/migrations/1710000000019-AddSessionMustReenroll.cjs +35 -0
- package/package.json +4 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"company-hierarchy.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company-hierarchy.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAkB,MAAM,mCAAmC,CAAC;AAUxF,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAHH,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,mBAAmB,EAC5B,KAAK,CAAC,EAAE,SAAS,YAAA,EACjB,GAAG,GAAE,MAAM,MAAiB;IAG/C,0EAA0E;IAC1E,OAAO,CAAC,OAAO;IAIf,uDAAuD;YACzC,KAAK;YAUL,WAAW;IAoBzB,iGAAiG;IAC3F,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAkD3F;;;OAGG;IACG,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc/D,wFAAwF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhD"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CompanyHierarchyService = void 0;
|
|
4
|
+
const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
|
|
5
|
+
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
6
|
+
const APP = 'shield_app';
|
|
7
|
+
class CompanyHierarchyService {
|
|
8
|
+
pool;
|
|
9
|
+
closure;
|
|
10
|
+
audit;
|
|
11
|
+
now;
|
|
12
|
+
constructor(pool, closure, audit, now = Date.now) {
|
|
13
|
+
this.pool = pool;
|
|
14
|
+
this.closure = closure;
|
|
15
|
+
this.audit = audit;
|
|
16
|
+
this.now = now;
|
|
17
|
+
}
|
|
18
|
+
/** The actor's own root node (the active node its subtree hangs from). */
|
|
19
|
+
ownRoot(actor) {
|
|
20
|
+
return actor.activeScopeNodeId ?? actor.tenantId;
|
|
21
|
+
}
|
|
22
|
+
/** Names of the given companies (unknown → absent). */
|
|
23
|
+
async names(q, ids) {
|
|
24
|
+
const unique = [...new Set(ids)];
|
|
25
|
+
if (unique.length === 0)
|
|
26
|
+
return new Map();
|
|
27
|
+
const rows = (await q.query(`SELECT c_uuid, c_name FROM \`${APP}\`.\`companies\` WHERE c_uuid IN (${unique.map(() => '?').join(', ')})`, unique));
|
|
28
|
+
return new Map(rows.map((r) => [r.c_uuid, r.c_name]));
|
|
29
|
+
}
|
|
30
|
+
async recordAudit(conn, actor, action, attributes) {
|
|
31
|
+
if (!this.audit)
|
|
32
|
+
return;
|
|
33
|
+
await this.audit.record({
|
|
34
|
+
type: 'admin_action',
|
|
35
|
+
occurredAtMs: this.now(),
|
|
36
|
+
actorId: actor.id,
|
|
37
|
+
tenantId: actor.tenantId,
|
|
38
|
+
action: { key: action },
|
|
39
|
+
attributes: { eventType: 'company_hierarchy', ...attributes },
|
|
40
|
+
}, (sql, params) => conn.query(sql, params));
|
|
41
|
+
}
|
|
42
|
+
/** Re-parent `nodeUuid` under `newParentUuid` (null = make it a top-level tenant; root only). */
|
|
43
|
+
async move(actor, nodeUuid, newParentUuid) {
|
|
44
|
+
if (typeof nodeUuid !== 'string' || nodeUuid.length === 0) {
|
|
45
|
+
throw new identity_errors_js_1.IdentityBadRequestError('A company is required.');
|
|
46
|
+
}
|
|
47
|
+
if (newParentUuid !== null &&
|
|
48
|
+
(typeof newParentUuid !== 'string' || newParentUuid.length === 0)) {
|
|
49
|
+
throw new identity_errors_js_1.IdentityBadRequestError('parentUuid must be a company or null.');
|
|
50
|
+
}
|
|
51
|
+
if (actor.isRoot !== true) {
|
|
52
|
+
const subtree = new Set((0, grant_ceiling_js_1.subtreeOf)(actor));
|
|
53
|
+
if (!subtree.has(nodeUuid))
|
|
54
|
+
throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
|
|
55
|
+
if (nodeUuid === this.ownRoot(actor)) {
|
|
56
|
+
throw new identity_errors_js_1.IdentityForbiddenError('You cannot move the organisation your access is rooted at.');
|
|
57
|
+
}
|
|
58
|
+
if (newParentUuid === null || !subtree.has(newParentUuid)) {
|
|
59
|
+
throw new identity_errors_js_1.IdentityForbiddenError('You can only move a company under another company you manage.');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const names = await this.names(this.pool, [
|
|
63
|
+
nodeUuid,
|
|
64
|
+
...(newParentUuid ? [newParentUuid] : []),
|
|
65
|
+
]);
|
|
66
|
+
if (!names.has(nodeUuid))
|
|
67
|
+
throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
|
|
68
|
+
if (newParentUuid !== null && !names.has(newParentUuid)) {
|
|
69
|
+
throw new identity_errors_js_1.IdentityNotFoundError('Parent company not found.');
|
|
70
|
+
}
|
|
71
|
+
const oldParentRows = (await this.pool.query(`SELECT c_parent_uuid FROM \`${APP}\`.\`companies\` WHERE c_uuid = ? LIMIT 1`, [nodeUuid]));
|
|
72
|
+
const oldParent = oldParentRows[0]?.c_parent_uuid ?? null;
|
|
73
|
+
const oldParentName = oldParent
|
|
74
|
+
? (await this.names(this.pool, [oldParent])).get(oldParent)
|
|
75
|
+
: undefined;
|
|
76
|
+
await this.closure.moveNode(nodeUuid, newParentUuid, (conn) => this.recordAudit(conn, actor, 'company.moved', {
|
|
77
|
+
company: names.get(nodeUuid),
|
|
78
|
+
from: oldParent ? (oldParentName ?? '(deleted organisation)') : '(top level)',
|
|
79
|
+
to: newParentUuid ? names.get(newParentUuid) : '(top level)',
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Delete a company through `deleteNode` (refuses a node with children; closure rows + ancestor
|
|
84
|
+
* bump in one transaction). The caller has already checked reach (walled read).
|
|
85
|
+
*/
|
|
86
|
+
async remove(actor, nodeUuid) {
|
|
87
|
+
// Security review F6: the same own-root guard as `move` — a sub-admin never deletes the
|
|
88
|
+
// organisation its access is rooted at.
|
|
89
|
+
if (actor.isRoot !== true && nodeUuid === this.ownRoot(actor)) {
|
|
90
|
+
throw new identity_errors_js_1.IdentityForbiddenError('You cannot delete the organisation your access is rooted at.');
|
|
91
|
+
}
|
|
92
|
+
const name = (await this.names(this.pool, [nodeUuid])).get(nodeUuid);
|
|
93
|
+
await this.closure.deleteNode(nodeUuid, (conn) => this.recordAudit(conn, actor, 'company.deleted', { company: name ?? '(unknown)' }));
|
|
94
|
+
}
|
|
95
|
+
/** A company was just created: give it closure rows now and bump its ancestors (D5). */
|
|
96
|
+
async attached(nodeUuid) {
|
|
97
|
+
await this.closure.attachNode(nodeUuid);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.CompanyHierarchyService = CompanyHierarchyService;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CompanyService } from './company.service.js';
|
|
2
2
|
import { type ListQueryDto } from '../shared/list-query.js';
|
|
3
|
+
import type { AuthedRequest } from '../shared/http.js';
|
|
3
4
|
export declare class CompanyController {
|
|
4
5
|
private readonly companies;
|
|
5
6
|
constructor(companies: CompanyService);
|
|
@@ -14,23 +15,40 @@ export declare class CompanyController {
|
|
|
14
15
|
page: number;
|
|
15
16
|
limit: number;
|
|
16
17
|
}>;
|
|
17
|
-
/**
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Create a company — gated on `identity:company:create`. A non-root caller's company is created
|
|
20
|
+
* UNDER the caller's own organisation (D5); only root creates a top-level tenant.
|
|
21
|
+
*/
|
|
22
|
+
createCompany(req: AuthedRequest, body: {
|
|
19
23
|
name?: string;
|
|
20
24
|
}): Promise<{
|
|
21
25
|
id: import("packages/engine/dist/types.js").RecordId;
|
|
22
26
|
}>;
|
|
23
27
|
/** Read one company — gated on `identity:company:read`. */
|
|
24
28
|
getCompany(uuid: string): Promise<import("packages/engine/dist/types.js").RecordView | null>;
|
|
25
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Move a company under a new parent — gated on `identity:company:update`. Body
|
|
31
|
+
* `{ parentUuid: string | null }`. Non-root: the company and the new parent must both be in the
|
|
32
|
+
* caller's subtree, the company is not the caller's own root, and the parent is not null. A move
|
|
33
|
+
* that would form a cycle → 409. Declared BEFORE `companies/:uuid` (static sub-path first).
|
|
34
|
+
*/
|
|
35
|
+
moveCompany(req: AuthedRequest, uuid: string, body: {
|
|
36
|
+
parentUuid?: string | null;
|
|
37
|
+
}): Promise<{
|
|
38
|
+
success: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Update a company's name/status — gated on `identity:company:update`. Any other key
|
|
42
|
+
* (parentUuid, type, …) is ignored: use `PUT companies/:uuid/parent` to move a company.
|
|
43
|
+
*/
|
|
26
44
|
updateCompany(uuid: string, body: {
|
|
27
45
|
name?: string;
|
|
28
46
|
status?: string;
|
|
29
47
|
}): Promise<{
|
|
30
48
|
success: boolean;
|
|
31
49
|
}>;
|
|
32
|
-
/** Delete a company — gated on `identity:company:delete`. */
|
|
33
|
-
deleteCompany(uuid: string): Promise<{
|
|
50
|
+
/** Delete a company — gated on `identity:company:delete`. A company with sub-companies → 409. */
|
|
51
|
+
deleteCompany(req: AuthedRequest, uuid: string): Promise<{
|
|
34
52
|
success: boolean;
|
|
35
53
|
}>;
|
|
36
54
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"company.controller.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"company.controller.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.controller.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvD,qBAEa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,cAAc;IAEtD;;;;OAIG;IAGG,aAAa,CAAU,KAAK,EAAE,YAAY;;;;;;IAMhD;;;OAGG;IAGG,aAAa,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAK9E,2DAA2D;IAG3D,UAAU,CAAgB,IAAI,EAAE,MAAM;IAItC;;;;;OAKG;IAGG,WAAW,CACR,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;IAa9C;;;OAGG;IAGG,aAAa,CACF,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMlD,iGAAiG;IAG3F,aAAa,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;CAI3E"}
|
|
@@ -17,6 +17,7 @@ const common_1 = require("@nestjs/common");
|
|
|
17
17
|
const guard_1 = require("@astram/guard");
|
|
18
18
|
const company_service_js_1 = require("./company.service.js");
|
|
19
19
|
const list_query_js_1 = require("../shared/list-query.js");
|
|
20
|
+
const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
|
|
20
21
|
let CompanyController = class CompanyController {
|
|
21
22
|
companies;
|
|
22
23
|
constructor(companies) {
|
|
@@ -32,23 +33,44 @@ let CompanyController = class CompanyController {
|
|
|
32
33
|
const result = await this.companies.listCompanies(params);
|
|
33
34
|
return { total: result.total, rows: result.rows, page, limit };
|
|
34
35
|
}
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Create a company — gated on `identity:company:create`. A non-root caller's company is created
|
|
38
|
+
* UNDER the caller's own organisation (D5); only root creates a top-level tenant.
|
|
39
|
+
*/
|
|
40
|
+
async createCompany(req, body) {
|
|
41
|
+
const id = await this.companies.createCompany(req.principal, body?.name ?? 'Untitled');
|
|
38
42
|
return { id };
|
|
39
43
|
}
|
|
40
44
|
/** Read one company — gated on `identity:company:read`. */
|
|
41
45
|
getCompany(uuid) {
|
|
42
46
|
return this.companies.getCompany(uuid);
|
|
43
47
|
}
|
|
44
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Move a company under a new parent — gated on `identity:company:update`. Body
|
|
50
|
+
* `{ parentUuid: string | null }`. Non-root: the company and the new parent must both be in the
|
|
51
|
+
* caller's subtree, the company is not the caller's own root, and the parent is not null. A move
|
|
52
|
+
* that would form a cycle → 409. Declared BEFORE `companies/:uuid` (static sub-path first).
|
|
53
|
+
*/
|
|
54
|
+
async moveCompany(req, uuid, body) {
|
|
55
|
+
if (!body ||
|
|
56
|
+
!('parentUuid' in body) ||
|
|
57
|
+
(body.parentUuid !== null && typeof body.parentUuid !== 'string')) {
|
|
58
|
+
throw new common_1.BadRequestException('parentUuid (a company id, or null) is required');
|
|
59
|
+
}
|
|
60
|
+
await this.companies.moveCompany(req.principal, uuid, body.parentUuid ?? null);
|
|
61
|
+
return { success: true };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Update a company's name/status — gated on `identity:company:update`. Any other key
|
|
65
|
+
* (parentUuid, type, …) is ignored: use `PUT companies/:uuid/parent` to move a company.
|
|
66
|
+
*/
|
|
45
67
|
async updateCompany(uuid, body) {
|
|
46
|
-
await this.companies.updateCompany(uuid, body
|
|
68
|
+
await this.companies.updateCompany(uuid, { name: body?.name, status: body?.status });
|
|
47
69
|
return { success: true };
|
|
48
70
|
}
|
|
49
|
-
/** Delete a company — gated on `identity:company:delete`. */
|
|
50
|
-
async deleteCompany(uuid) {
|
|
51
|
-
await this.companies.deleteCompany(uuid);
|
|
71
|
+
/** Delete a company — gated on `identity:company:delete`. A company with sub-companies → 409. */
|
|
72
|
+
async deleteCompany(req, uuid) {
|
|
73
|
+
await this.companies.deleteCompany(req.principal, uuid);
|
|
52
74
|
return { success: true };
|
|
53
75
|
}
|
|
54
76
|
};
|
|
@@ -64,9 +86,10 @@ __decorate([
|
|
|
64
86
|
__decorate([
|
|
65
87
|
(0, common_1.Post)('companies'),
|
|
66
88
|
(0, guard_1.Classified)({ typeKey: 'company', action: 'create' }),
|
|
67
|
-
__param(0, (0, common_1.
|
|
89
|
+
__param(0, (0, common_1.Req)()),
|
|
90
|
+
__param(1, (0, common_1.Body)()),
|
|
68
91
|
__metadata("design:type", Function),
|
|
69
|
-
__metadata("design:paramtypes", [Object]),
|
|
92
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
70
93
|
__metadata("design:returntype", Promise)
|
|
71
94
|
], CompanyController.prototype, "createCompany", null);
|
|
72
95
|
__decorate([
|
|
@@ -77,6 +100,16 @@ __decorate([
|
|
|
77
100
|
__metadata("design:paramtypes", [String]),
|
|
78
101
|
__metadata("design:returntype", void 0)
|
|
79
102
|
], CompanyController.prototype, "getCompany", null);
|
|
103
|
+
__decorate([
|
|
104
|
+
(0, common_1.Put)('companies/:uuid/parent'),
|
|
105
|
+
(0, guard_1.Classified)({ typeKey: 'company', action: 'update' }),
|
|
106
|
+
__param(0, (0, common_1.Req)()),
|
|
107
|
+
__param(1, (0, common_1.Param)('uuid')),
|
|
108
|
+
__param(2, (0, common_1.Body)()),
|
|
109
|
+
__metadata("design:type", Function),
|
|
110
|
+
__metadata("design:paramtypes", [Object, String, Object]),
|
|
111
|
+
__metadata("design:returntype", Promise)
|
|
112
|
+
], CompanyController.prototype, "moveCompany", null);
|
|
80
113
|
__decorate([
|
|
81
114
|
(0, common_1.Put)('companies/:uuid'),
|
|
82
115
|
(0, guard_1.Classified)({ typeKey: 'company', action: 'update' }),
|
|
@@ -89,12 +122,14 @@ __decorate([
|
|
|
89
122
|
__decorate([
|
|
90
123
|
(0, common_1.Delete)('companies/:uuid'),
|
|
91
124
|
(0, guard_1.Classified)({ typeKey: 'company', action: 'delete' }),
|
|
92
|
-
__param(0, (0, common_1.
|
|
125
|
+
__param(0, (0, common_1.Req)()),
|
|
126
|
+
__param(1, (0, common_1.Param)('uuid')),
|
|
93
127
|
__metadata("design:type", Function),
|
|
94
|
-
__metadata("design:paramtypes", [String]),
|
|
128
|
+
__metadata("design:paramtypes", [Object, String]),
|
|
95
129
|
__metadata("design:returntype", Promise)
|
|
96
130
|
], CompanyController.prototype, "deleteCompany", null);
|
|
97
131
|
exports.CompanyController = CompanyController = __decorate([
|
|
98
132
|
(0, common_1.Controller)('identity'),
|
|
133
|
+
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
99
134
|
__metadata("design:paramtypes", [company_service_js_1.CompanyService])
|
|
100
135
|
], CompanyController);
|
|
@@ -1,25 +1,41 @@
|
|
|
1
1
|
import { RecordsService, type RecordId } from '@astram/engine';
|
|
2
|
+
import type { Principal } from '@astram/core';
|
|
2
3
|
import { type ListParams } from '../shared/list-query.js';
|
|
4
|
+
import { CompanyHierarchyService } from './company-hierarchy.service.js';
|
|
3
5
|
/**
|
|
4
6
|
* CompanyService — company (tenant) CRUD on the paved road, via the framework's
|
|
5
|
-
* RecordsService facade
|
|
6
|
-
*
|
|
7
|
+
* RecordsService facade (the request principal acts). No raw SQL, no RECORD_ENGINE.
|
|
8
|
+
*
|
|
9
|
+
* Hierarchy changes never ride a generic write (security hardening #2): the patch is whitelisted
|
|
10
|
+
* to `{name, status}`; create, move and delete keep the scope closure consistent through
|
|
11
|
+
* {@link CompanyHierarchyService}.
|
|
7
12
|
*/
|
|
8
13
|
export declare class CompanyService {
|
|
9
14
|
private readonly records;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
private readonly hierarchy;
|
|
16
|
+
constructor(records: RecordsService, hierarchy: CompanyHierarchyService);
|
|
17
|
+
/**
|
|
18
|
+
* Create a company. A non-root creator's company is placed UNDER the creator's own node (D5) —
|
|
19
|
+
* never a new top-level tenant — and the closure is rebuilt + the ancestors bumped at once so
|
|
20
|
+
* the new node is inside the creator's subtree (their session picks it up at its next refresh).
|
|
21
|
+
* Root creates a top-level tenant.
|
|
22
|
+
*/
|
|
23
|
+
createCompany(actor: Principal, name: string): Promise<RecordId>;
|
|
24
|
+
private readonly logger;
|
|
13
25
|
/** List companies (tenant-scoped by the pipe), with paging/search/sort. */
|
|
14
26
|
listCompanies(params?: ListParams): Promise<import("@astram/engine").RecordPage>;
|
|
15
27
|
/** Read a single company. */
|
|
16
28
|
getCompany(uuid: string): Promise<import("@astram/engine").RecordView | null>;
|
|
17
|
-
/** Update a company. */
|
|
18
|
-
updateCompany(uuid: string, patch:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
/** Update a company's name/status. Any other key (parentUuid, type, …) is dropped. */
|
|
30
|
+
updateCompany(uuid: string, patch: Readonly<Record<string, unknown>>): Promise<void>;
|
|
31
|
+
/** Move a company under a new parent (hierarchy rules + cycle check, see the hierarchy service). */
|
|
32
|
+
moveCompany(actor: Principal, uuid: string, parentUuid: string | null): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Delete a company: reach is checked through the subtree read wall (out of reach → 404), then
|
|
35
|
+
* the node is removed through `deleteNode` (refuses a node with children; closure + ancestor
|
|
36
|
+
* bump + audit in one transaction) — never a raw engine delete that would leave the closure
|
|
37
|
+
* stale or silently promote children.
|
|
38
|
+
*/
|
|
39
|
+
deleteCompany(actor: Principal, uuid: string): Promise<void>;
|
|
24
40
|
}
|
|
25
41
|
//# sourceMappingURL=company.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"company.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"company.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AASzE;;;;;;;GAOG;AACH,qBACa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,uBAAuB;IAGrD;;;;;OAKG;IACG,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAetE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAE1D,2EAA2E;IAC3E,aAAa,CAAC,MAAM,CAAC,EAAE,UAAU;IAOjC,6BAA6B;IAC7B,UAAU,CAAC,IAAI,EAAE,MAAM;IAIvB,sFAAsF;IACtF,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAQpF,oGAAoG;IACpG,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrF;;;;;OAKG;IACG,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAKnE"}
|
|
@@ -8,27 +8,56 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
|
+
var CompanyService_1;
|
|
11
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
13
|
exports.CompanyService = void 0;
|
|
13
14
|
const common_1 = require("@nestjs/common");
|
|
14
15
|
const engine_1 = require("@astram/engine");
|
|
15
16
|
const list_query_js_1 = require("../shared/list-query.js");
|
|
17
|
+
const company_hierarchy_service_js_1 = require("./company-hierarchy.service.js");
|
|
18
|
+
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
16
19
|
const COMPANY_SEARCH_FIELDS = ['name'];
|
|
17
20
|
const COMPANY_SORT_FIELDS = ['name', 'status', 'createdAt'];
|
|
21
|
+
/** The only company fields a generic patch may change (security hardening #2). */
|
|
22
|
+
const COMPANY_PATCH_KEYS = ['name', 'status'];
|
|
18
23
|
/**
|
|
19
24
|
* CompanyService — company (tenant) CRUD on the paved road, via the framework's
|
|
20
|
-
* RecordsService facade
|
|
21
|
-
*
|
|
25
|
+
* RecordsService facade (the request principal acts). No raw SQL, no RECORD_ENGINE.
|
|
26
|
+
*
|
|
27
|
+
* Hierarchy changes never ride a generic write (security hardening #2): the patch is whitelisted
|
|
28
|
+
* to `{name, status}`; create, move and delete keep the scope closure consistent through
|
|
29
|
+
* {@link CompanyHierarchyService}.
|
|
22
30
|
*/
|
|
23
|
-
let CompanyService = class CompanyService {
|
|
31
|
+
let CompanyService = CompanyService_1 = class CompanyService {
|
|
24
32
|
records;
|
|
25
|
-
|
|
33
|
+
hierarchy;
|
|
34
|
+
constructor(records, hierarchy) {
|
|
26
35
|
this.records = records;
|
|
36
|
+
this.hierarchy = hierarchy;
|
|
27
37
|
}
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Create a company. A non-root creator's company is placed UNDER the creator's own node (D5) —
|
|
40
|
+
* never a new top-level tenant — and the closure is rebuilt + the ancestors bumped at once so
|
|
41
|
+
* the new node is inside the creator's subtree (their session picks it up at its next refresh).
|
|
42
|
+
* Root creates a top-level tenant.
|
|
43
|
+
*/
|
|
44
|
+
async createCompany(actor, name) {
|
|
45
|
+
const fields = { name };
|
|
46
|
+
if (actor.isRoot !== true)
|
|
47
|
+
fields.parentUuid = actor.activeScopeNodeId ?? actor.tenantId;
|
|
48
|
+
const id = await this.records.create('company', { fields });
|
|
49
|
+
try {
|
|
50
|
+
await this.hierarchy.attached(String(id));
|
|
51
|
+
}
|
|
52
|
+
catch (cause) {
|
|
53
|
+
// The company row is committed; a failed closure rebuild only DELAYS the new node's reach
|
|
54
|
+
// (fail-closed: no closure row ⇒ nobody's subtree includes it) until the next reconcile.
|
|
55
|
+
// Answering an error here would invite a duplicate create on retry.
|
|
56
|
+
this.logger.error(`company closure attach failed: ${cause?.name ?? 'error'}`);
|
|
57
|
+
}
|
|
58
|
+
return id;
|
|
31
59
|
}
|
|
60
|
+
logger = new common_1.Logger(CompanyService_1.name);
|
|
32
61
|
/** List companies (tenant-scoped by the pipe), with paging/search/sort. */
|
|
33
62
|
listCompanies(params) {
|
|
34
63
|
return this.records.list('company', (0, list_query_js_1.buildListQuery)(params, COMPANY_SEARCH_FIELDS, COMPANY_SORT_FIELDS));
|
|
@@ -37,17 +66,35 @@ let CompanyService = class CompanyService {
|
|
|
37
66
|
getCompany(uuid) {
|
|
38
67
|
return this.records.get('company', uuid);
|
|
39
68
|
}
|
|
40
|
-
/** Update a company. */
|
|
69
|
+
/** Update a company's name/status. Any other key (parentUuid, type, …) is dropped. */
|
|
41
70
|
updateCompany(uuid, patch) {
|
|
42
|
-
|
|
71
|
+
const fields = {};
|
|
72
|
+
for (const key of COMPANY_PATCH_KEYS) {
|
|
73
|
+
if (typeof patch?.[key] === 'string')
|
|
74
|
+
fields[key] = patch[key];
|
|
75
|
+
}
|
|
76
|
+
return this.records.update('company', uuid, { fields });
|
|
43
77
|
}
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
return this.
|
|
78
|
+
/** Move a company under a new parent (hierarchy rules + cycle check, see the hierarchy service). */
|
|
79
|
+
moveCompany(actor, uuid, parentUuid) {
|
|
80
|
+
return this.hierarchy.move(actor, uuid, parentUuid);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Delete a company: reach is checked through the subtree read wall (out of reach → 404), then
|
|
84
|
+
* the node is removed through `deleteNode` (refuses a node with children; closure + ancestor
|
|
85
|
+
* bump + audit in one transaction) — never a raw engine delete that would leave the closure
|
|
86
|
+
* stale or silently promote children.
|
|
87
|
+
*/
|
|
88
|
+
async deleteCompany(actor, uuid) {
|
|
89
|
+
const company = await this.records.get('company', uuid);
|
|
90
|
+
if (company === null)
|
|
91
|
+
throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
|
|
92
|
+
await this.hierarchy.remove(actor, uuid);
|
|
47
93
|
}
|
|
48
94
|
};
|
|
49
95
|
exports.CompanyService = CompanyService;
|
|
50
|
-
exports.CompanyService = CompanyService = __decorate([
|
|
96
|
+
exports.CompanyService = CompanyService = CompanyService_1 = __decorate([
|
|
51
97
|
(0, common_1.Injectable)(),
|
|
52
|
-
__metadata("design:paramtypes", [engine_1.RecordsService
|
|
98
|
+
__metadata("design:paramtypes", [engine_1.RecordsService,
|
|
99
|
+
company_hierarchy_service_js_1.CompanyHierarchyService])
|
|
53
100
|
], CompanyService);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cross-tenant-grant.controller.d.ts","sourceRoot":"","sources":["../../../src/features/cross-tenant/cross-tenant-grant.controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cross-tenant-grant.controller.d.ts","sourceRoot":"","sources":["../../../src/features/cross-tenant/cross-tenant-grant.controller.ts"],"names":[],"mappings":"AA8CA,OAAO,EAGL,sBAAsB,EAKvB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AAEnF,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,gGAAgG;AAChG,UAAU,iBAAiB;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAqDD,qBAEa,0BAA0B;IAEO,OAAO,CAAC,QAAQ,CAAC,WAAW;IACtE,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADqC,WAAW,EAAE,sBAAsB,EAC7E,KAAK,EAAE,2BAA2B;IAGrD;;;;OAIG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,iBAAiB;;;;IA+BtE,yFAAyF;IAGnF,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;IASnE;;;;;OAKG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;IASnE;;;OAGG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa;;;IAKrC;;;;OAIG;IAGG,IAAI,CAAQ,GAAG,EAAE,aAAa;;;;IAQpC,wFAAwF;IAGlF,cAAc;;;CAIrB"}
|
|
@@ -46,6 +46,7 @@ exports.CrossTenantGrantController = void 0;
|
|
|
46
46
|
* writer change).
|
|
47
47
|
*/
|
|
48
48
|
const common_1 = require("@nestjs/common");
|
|
49
|
+
const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
|
|
49
50
|
const guard_1 = require("@astram/guard");
|
|
50
51
|
const cross_tenant_grant_read_service_js_1 = require("./cross-tenant-grant-read.service.js");
|
|
51
52
|
const VALID_GRANTEE_KINDS = ['user', 'role'];
|
|
@@ -231,6 +232,7 @@ __decorate([
|
|
|
231
232
|
], CrossTenantGrantController.prototype, "offerableTypes", null);
|
|
232
233
|
exports.CrossTenantGrantController = CrossTenantGrantController = __decorate([
|
|
233
234
|
(0, common_1.Controller)('identity'),
|
|
235
|
+
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
234
236
|
__param(0, (0, common_1.Inject)(guard_1.SHIELD_CROSS_TENANT_GRANT_WRITER)),
|
|
235
237
|
__metadata("design:paramtypes", [guard_1.CrossTenantGrantWriter,
|
|
236
238
|
cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAYL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AA6FxB;;;;;;;;;;GAUG;AACH,qBACa,qBAAqB;IAChC;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,OAAO,CAAC,EACb,OAAO,EACP,YAAiB,GAClB,GAAE;QACD,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,YAAY,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;KACtC,GAAG,aAAa;CAscvB"}
|