@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,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ServiceAccountStore = void 0;
|
|
4
|
+
const APP = 'shield_app';
|
|
5
|
+
/** Normalize a JSON-array column (driver returns array OR string) → string[]. Fail-safe to []. */
|
|
6
|
+
function jsonArray(v) {
|
|
7
|
+
const arr = Array.isArray(v)
|
|
8
|
+
? v
|
|
9
|
+
: typeof v === 'string'
|
|
10
|
+
? (() => {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(v);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
})()
|
|
18
|
+
: [];
|
|
19
|
+
return Array.isArray(arr) ? arr.filter((x) => typeof x === 'string') : [];
|
|
20
|
+
}
|
|
21
|
+
function toRow(r) {
|
|
22
|
+
return {
|
|
23
|
+
id: r.sa_id,
|
|
24
|
+
tenantId: r.sa_tenant_id,
|
|
25
|
+
label: r.sa_label,
|
|
26
|
+
scopeSubtreeNodeIds: jsonArray(r.sa_scope_subtree),
|
|
27
|
+
roleIds: jsonArray(r.sa_role_ids),
|
|
28
|
+
status: r.sa_status,
|
|
29
|
+
createdBy: r.sa_created_by,
|
|
30
|
+
createdAtMs: Number(r.sa_created_at_ms),
|
|
31
|
+
updatedAtMs: r.sa_updated_at_ms === null ? null : Number(r.sa_updated_at_ms),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the own-tenant `AND sa_tenant_id IN (?, …)` fragment for a non-root caller. Root → no
|
|
36
|
+
* fragment (spans all). A non-root with an empty subtree yields `AND 1=0` (matches nothing —
|
|
37
|
+
* fail-closed). Node ids are `?`-bound. The caller splices `params` where `sql` sits.
|
|
38
|
+
*/
|
|
39
|
+
function tenantScope(scope) {
|
|
40
|
+
if (scope.isRoot)
|
|
41
|
+
return { sql: '', params: [] };
|
|
42
|
+
if (scope.nodeIds.length === 0)
|
|
43
|
+
return { sql: ' AND 1=0', params: [] };
|
|
44
|
+
const ph = scope.nodeIds.map(() => '?').join(', ');
|
|
45
|
+
return { sql: ` AND sa_tenant_id IN (${ph})`, params: [...scope.nodeIds] };
|
|
46
|
+
}
|
|
47
|
+
const COLS = 'sa_id, sa_tenant_id, sa_label, sa_scope_subtree, sa_role_ids, sa_status, sa_created_by, sa_created_at_ms, sa_updated_at_ms';
|
|
48
|
+
/** Dedupe an id list keeping first-seen order (drops empties). */
|
|
49
|
+
function uniqueIds(ids) {
|
|
50
|
+
return [...new Set(ids.filter((x) => typeof x === 'string' && x.length > 0))];
|
|
51
|
+
}
|
|
52
|
+
class ServiceAccountStore {
|
|
53
|
+
pool;
|
|
54
|
+
constructor(pool) {
|
|
55
|
+
this.pool = pool;
|
|
56
|
+
}
|
|
57
|
+
// ── directory reads (names for the picker, the view labels and the ceiling messages) ─────
|
|
58
|
+
// Read-only lookups over the global role catalog + the company tree (`shield_app.roles`,
|
|
59
|
+
// `shield_app.companies`). Ids are `?`-bound; an empty id list short-circuits (no query).
|
|
60
|
+
/** Roles by id (unknown ids are simply absent — the caller supplies a fallback label). */
|
|
61
|
+
async rolesByIds(ids) {
|
|
62
|
+
const u = uniqueIds(ids);
|
|
63
|
+
if (u.length === 0)
|
|
64
|
+
return [];
|
|
65
|
+
const rows = (await this.pool.query(`SELECT r_uuid AS id, r_name AS name FROM \`${APP}\`.\`roles\` WHERE r_uuid IN (${u.map(() => '?').join(', ')}) ORDER BY r_name`, u));
|
|
66
|
+
return rows.map((r) => ({ id: r.id, name: r.name }));
|
|
67
|
+
}
|
|
68
|
+
/** Every role in the catalog (ROOT's assignable set). */
|
|
69
|
+
async allRoles() {
|
|
70
|
+
const rows = (await this.pool.query(`SELECT r_uuid AS id, r_name AS name FROM \`${APP}\`.\`roles\` ORDER BY r_name`, []));
|
|
71
|
+
return rows.map((r) => ({ id: r.id, name: r.name }));
|
|
72
|
+
}
|
|
73
|
+
/** Companies (scope nodes) by id, with their parent handle. Unknown ids are absent. */
|
|
74
|
+
async nodesByIds(ids) {
|
|
75
|
+
const u = uniqueIds(ids);
|
|
76
|
+
if (u.length === 0)
|
|
77
|
+
return [];
|
|
78
|
+
const rows = (await this.pool.query(`SELECT c_uuid AS id, c_name AS name, c_parent_uuid AS parentId FROM \`${APP}\`.\`companies\` WHERE c_uuid IN (${u.map(() => '?').join(', ')}) ORDER BY c_name`, u));
|
|
79
|
+
return rows.map((r) => ({ id: r.id, name: r.name, parentId: r.parentId ?? null }));
|
|
80
|
+
}
|
|
81
|
+
/** Every company (ROOT's assignable scope — root reaches all). */
|
|
82
|
+
async allNodes() {
|
|
83
|
+
const rows = (await this.pool.query(`SELECT c_uuid AS id, c_name AS name, c_parent_uuid AS parentId FROM \`${APP}\`.\`companies\` ORDER BY c_name`, []));
|
|
84
|
+
return rows.map((r) => ({ id: r.id, name: r.name, parentId: r.parentId ?? null }));
|
|
85
|
+
}
|
|
86
|
+
/** List the caller's own-tenant accounts (newest first). Root sees all. */
|
|
87
|
+
async list(scope) {
|
|
88
|
+
const t = tenantScope(scope);
|
|
89
|
+
const rows = (await this.pool.query(`SELECT ${COLS} FROM \`${APP}\`.\`service_account\` WHERE 1=1${t.sql} ORDER BY sa_created_at_ms DESC`, t.params));
|
|
90
|
+
return rows.map(toRow);
|
|
91
|
+
}
|
|
92
|
+
/** Fetch one account BY id, scoped to the caller's subtree. Cross-tenant/unknown → null (404). */
|
|
93
|
+
async getById(id, scope) {
|
|
94
|
+
const t = tenantScope(scope);
|
|
95
|
+
const rows = (await this.pool.query(`SELECT ${COLS} FROM \`${APP}\`.\`service_account\` WHERE sa_id = ?${t.sql} LIMIT 1`, [id, ...t.params]));
|
|
96
|
+
return rows[0] ? toRow(rows[0]) : null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Insert a new account. `tenantId` is server-stamped (from the session) — never client input.
|
|
100
|
+
* `scopeSubtreeNodeIds`/`roleIds` are stored as JSON. Idempotency is the caller's (a fresh uuid
|
|
101
|
+
* per create); the id is unique by construction.
|
|
102
|
+
*/
|
|
103
|
+
async insert(input) {
|
|
104
|
+
await this.pool.query(`INSERT INTO \`${APP}\`.\`service_account\`
|
|
105
|
+
(sa_id, sa_tenant_id, sa_label, sa_scope_subtree, sa_role_ids, sa_status,
|
|
106
|
+
sa_created_by, sa_created_at_ms, sa_updated_at_ms)
|
|
107
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)`, [
|
|
108
|
+
input.id,
|
|
109
|
+
input.tenantId,
|
|
110
|
+
input.label,
|
|
111
|
+
JSON.stringify([...input.scopeSubtreeNodeIds]),
|
|
112
|
+
JSON.stringify([...input.roleIds]),
|
|
113
|
+
input.status,
|
|
114
|
+
input.createdBy,
|
|
115
|
+
input.createdAtMs,
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Update an account's mutable fields (label/scope/roles/status), scoped to the caller's subtree.
|
|
120
|
+
* Returns the number of rows changed — 0 means the id was not in the caller's subtree (→ 404).
|
|
121
|
+
* Only the provided fields are set; `sa_updated_at_ms` is always stamped.
|
|
122
|
+
*/
|
|
123
|
+
async update(id, scope, patch, updatedAtMs,
|
|
124
|
+
/**
|
|
125
|
+
* Optimistic guard: when given, the row is only written if `sa_updated_at_ms` still equals this
|
|
126
|
+
* value (NULL-safe). A concurrent edit makes it return 0, so a read-modify-write can't resurrect
|
|
127
|
+
* access another admin just removed.
|
|
128
|
+
*/
|
|
129
|
+
expectedUpdatedAtMs) {
|
|
130
|
+
const sets = [];
|
|
131
|
+
const params = [];
|
|
132
|
+
if (patch.label !== undefined) {
|
|
133
|
+
sets.push('sa_label = ?');
|
|
134
|
+
params.push(patch.label);
|
|
135
|
+
}
|
|
136
|
+
if (patch.scopeSubtreeNodeIds !== undefined) {
|
|
137
|
+
sets.push('sa_scope_subtree = ?');
|
|
138
|
+
params.push(JSON.stringify([...patch.scopeSubtreeNodeIds]));
|
|
139
|
+
}
|
|
140
|
+
if (patch.roleIds !== undefined) {
|
|
141
|
+
sets.push('sa_role_ids = ?');
|
|
142
|
+
params.push(JSON.stringify([...patch.roleIds]));
|
|
143
|
+
}
|
|
144
|
+
if (patch.status !== undefined) {
|
|
145
|
+
sets.push('sa_status = ?');
|
|
146
|
+
params.push(patch.status);
|
|
147
|
+
}
|
|
148
|
+
// Always stamp the edit time (even for a no-field patch, which the service disallows anyway).
|
|
149
|
+
sets.push('sa_updated_at_ms = ?');
|
|
150
|
+
params.push(updatedAtMs);
|
|
151
|
+
const t = tenantScope(scope);
|
|
152
|
+
const guard = expectedUpdatedAtMs !== undefined ? ' AND sa_updated_at_ms <=> ?' : '';
|
|
153
|
+
const guardParams = expectedUpdatedAtMs !== undefined ? [expectedUpdatedAtMs] : [];
|
|
154
|
+
const res = (await this.pool.query(`UPDATE \`${APP}\`.\`service_account\` SET ${sets.join(', ')} WHERE sa_id = ?${t.sql}${guard}`, [...params, id, ...t.params, ...guardParams]));
|
|
155
|
+
return res.affectedRows ?? 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.ServiceAccountStore = ServiceAccountStore;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"share.controller.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"share.controller.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.controller.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AAErE,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAwBD,qBAEa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,YAAY;IAEjD;;;;OAIG;IAGG,IAAI,CACD,GAAG,EAAE,aAAa,EACP,OAAO,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM;;;IAYtC;;;;OAIG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;IAW5D;;;OAGG;IAGG,OAAO,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;CAU/D"}
|
|
@@ -28,6 +28,7 @@ exports.ShareController = void 0;
|
|
|
28
28
|
*/
|
|
29
29
|
const common_1 = require("@nestjs/common");
|
|
30
30
|
const guard_1 = require("@astram/guard");
|
|
31
|
+
const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
|
|
31
32
|
const share_service_js_1 = require("./share.service.js");
|
|
32
33
|
/** Validate + narrow a share/unshare body to a {@link ShareRequest} (grantee + action only). */
|
|
33
34
|
function toShareRequest(body) {
|
|
@@ -139,5 +140,6 @@ __decorate([
|
|
|
139
140
|
], ShareController.prototype, "unshare", null);
|
|
140
141
|
exports.ShareController = ShareController = __decorate([
|
|
141
142
|
(0, common_1.Controller)('identity'),
|
|
143
|
+
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
142
144
|
__metadata("design:paramtypes", [share_service_js_1.ShareService])
|
|
143
145
|
], ShareController);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"share.service.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"share.service.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEzE,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrF;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,KAAK,EAAE;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,OAAO,CAAC,KAAK,EAAE;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,mBAAmB,CAAC,KAAK,EAAE;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CACT,aAAa,CAAC;QACZ,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CACH,CAAC;CACH;AAED,oFAAoF;AACpF,MAAM,WAAW,SAAS;IACxB,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,kGAAkG;AAClG,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,MAAM,EAAE,iBAAiB,GAAG,eAAe,EAC3C,MAAM,EAAE,kBAAkB;IAG7C;;;;OAIG;IACG,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/D,qFAAqF;IAC/E,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjE;;;OAGG;IACG,UAAU,CACd,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,SAAS,EAAE,CAAC;CAoBjC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IdentityErrorInterceptor — maps engine, ceiling and identity domain errors to HTTP for every
|
|
3
|
+
* identity route, in ONE place (security hardening Phase 1, ADR-0034 follow-up).
|
|
4
|
+
*
|
|
5
|
+
* Before this, an engine `AuthorizationDeniedError` (for example the ADR-0034 create wall's
|
|
6
|
+
* `create_outside_write_scope`) escaped a controller as an unmapped error → HTTP 500. Each mapping
|
|
7
|
+
* below uses a fixed, id-free message (ADR-0012): the engine's own message carries reason codes and
|
|
8
|
+
* detail that are for logs, not clients.
|
|
9
|
+
*
|
|
10
|
+
* Applied with `@UseInterceptors(IdentityErrorInterceptor)` on each identity controller. An
|
|
11
|
+
* `HttpException` (a controller's own 400/403/404) passes through untouched; anything unmapped is
|
|
12
|
+
* rethrown as-is (Nest's default handling).
|
|
13
|
+
*/
|
|
14
|
+
import { type CallHandler, type ExecutionContext, type NestInterceptor } from '@nestjs/common';
|
|
15
|
+
import { type Observable } from 'rxjs';
|
|
16
|
+
/**
|
|
17
|
+
* Map one error to the HTTP exception identity answers with, or return it unchanged when it is
|
|
18
|
+
* not an identity-mapped type. Exported for unit tests and for callers that map by hand.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toIdentityHttpError(err: unknown): unknown;
|
|
21
|
+
export declare class IdentityErrorInterceptor implements NestInterceptor {
|
|
22
|
+
intercept(_context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=identity-error.interceptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity-error.interceptor.d.ts","sourceRoot":"","sources":["../../../src/features/shared/identity-error.interceptor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAQL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAqB/D;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAwBzD;AAED,qBACa,wBAAyB,YAAW,eAAe;IAC9D,SAAS,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC,OAAO,CAAC;CAK9E"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.IdentityErrorInterceptor = void 0;
|
|
10
|
+
exports.toIdentityHttpError = toIdentityHttpError;
|
|
11
|
+
/**
|
|
12
|
+
* IdentityErrorInterceptor — maps engine, ceiling and identity domain errors to HTTP for every
|
|
13
|
+
* identity route, in ONE place (security hardening Phase 1, ADR-0034 follow-up).
|
|
14
|
+
*
|
|
15
|
+
* Before this, an engine `AuthorizationDeniedError` (for example the ADR-0034 create wall's
|
|
16
|
+
* `create_outside_write_scope`) escaped a controller as an unmapped error → HTTP 500. Each mapping
|
|
17
|
+
* below uses a fixed, id-free message (ADR-0012): the engine's own message carries reason codes and
|
|
18
|
+
* detail that are for logs, not clients.
|
|
19
|
+
*
|
|
20
|
+
* Applied with `@UseInterceptors(IdentityErrorInterceptor)` on each identity controller. An
|
|
21
|
+
* `HttpException` (a controller's own 400/403/404) passes through untouched; anything unmapped is
|
|
22
|
+
* rethrown as-is (Nest's default handling).
|
|
23
|
+
*/
|
|
24
|
+
const common_1 = require("@nestjs/common");
|
|
25
|
+
const rxjs_1 = require("rxjs");
|
|
26
|
+
const engine_1 = require("@astram/engine");
|
|
27
|
+
const core_1 = require("@astram/core");
|
|
28
|
+
const guard_1 = require("@astram/guard");
|
|
29
|
+
const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
|
|
30
|
+
const scope_closure_service_js_1 = require("../scope/scope-closure.service.js");
|
|
31
|
+
const identity_errors_js_1 = require("./identity-errors.js");
|
|
32
|
+
/** The fixed client message for an engine deny, by reason code (never the engine's own text). */
|
|
33
|
+
function deniedMessage(reason) {
|
|
34
|
+
if (reason === 'create_outside_write_scope') {
|
|
35
|
+
return 'You cannot create this record: it is outside the organisations you manage.';
|
|
36
|
+
}
|
|
37
|
+
return 'You do not have access to perform this action.';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Map one error to the HTTP exception identity answers with, or return it unchanged when it is
|
|
41
|
+
* not an identity-mapped type. Exported for unit tests and for callers that map by hand.
|
|
42
|
+
*/
|
|
43
|
+
function toIdentityHttpError(err) {
|
|
44
|
+
if (err instanceof common_1.HttpException)
|
|
45
|
+
return err;
|
|
46
|
+
if (err instanceof engine_1.AuthorizationDeniedError)
|
|
47
|
+
return new common_1.ForbiddenException(deniedMessage(err.reason));
|
|
48
|
+
if (err instanceof grant_ceiling_js_1.GrantCeilingError)
|
|
49
|
+
return new common_1.ForbiddenException(err.message);
|
|
50
|
+
if (err instanceof identity_errors_js_1.IdentityForbiddenError)
|
|
51
|
+
return new common_1.ForbiddenException(err.message);
|
|
52
|
+
if (err instanceof identity_errors_js_1.IdentityNotFoundError)
|
|
53
|
+
return new common_1.NotFoundException(err.message);
|
|
54
|
+
if (err instanceof identity_errors_js_1.IdentityConflictError)
|
|
55
|
+
return new common_1.ConflictException(err.message);
|
|
56
|
+
if (err instanceof identity_errors_js_1.IdentityBadRequestError)
|
|
57
|
+
return new common_1.BadRequestException(err.message);
|
|
58
|
+
if (err instanceof guard_1.ScopeCycleError) {
|
|
59
|
+
return new common_1.ConflictException('That move would put an organisation inside its own sub-tree.');
|
|
60
|
+
}
|
|
61
|
+
if (err instanceof scope_closure_service_js_1.NodeHasChildrenError) {
|
|
62
|
+
return new common_1.ConflictException('This organisation still has sub-organisations. Move or delete them first.');
|
|
63
|
+
}
|
|
64
|
+
if (err instanceof engine_1.ValidationError) {
|
|
65
|
+
return new common_1.BadRequestException(`Invalid value for '${err.field}'.`);
|
|
66
|
+
}
|
|
67
|
+
if (err instanceof core_1.AuthorizationUnavailableError || err instanceof engine_1.EngineUnavailableError) {
|
|
68
|
+
return new common_1.ServiceUnavailableException('Authorization is temporarily unavailable. Try again.');
|
|
69
|
+
}
|
|
70
|
+
return err;
|
|
71
|
+
}
|
|
72
|
+
let IdentityErrorInterceptor = class IdentityErrorInterceptor {
|
|
73
|
+
intercept(_context, next) {
|
|
74
|
+
return next
|
|
75
|
+
.handle()
|
|
76
|
+
.pipe((0, rxjs_1.catchError)((err) => (0, rxjs_1.throwError)(() => toIdentityHttpError(err))));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
exports.IdentityErrorInterceptor = IdentityErrorInterceptor;
|
|
80
|
+
exports.IdentityErrorInterceptor = IdentityErrorInterceptor = __decorate([
|
|
81
|
+
(0, common_1.Injectable)()
|
|
82
|
+
], IdentityErrorInterceptor);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity domain errors (security hardening Phase 1). Thrown by identity services, mapped to HTTP
|
|
3
|
+
* by {@link IdentityErrorInterceptor} in ONE place so every identity route answers the same way.
|
|
4
|
+
*
|
|
5
|
+
* Messages are user-facing: they name no raw ids (ADR-0012) and carry no store detail.
|
|
6
|
+
*/
|
|
7
|
+
/** The caller may not perform this action on this target (→ 403). */
|
|
8
|
+
export declare class IdentityForbiddenError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
/** The target does not exist or is outside the caller's reach (→ 404, no existence leak). */
|
|
12
|
+
export declare class IdentityNotFoundError extends Error {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
/** The request conflicts with the current state (cycle, children present, … → 409). */
|
|
16
|
+
export declare class IdentityConflictError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
/** The request is malformed (→ 400). */
|
|
20
|
+
export declare class IdentityBadRequestError extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=identity-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity-errors.d.ts","sourceRoot":"","sources":["../../../src/features/shared/identity-errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,qEAAqE;AACrE,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,6FAA6F;AAC7F,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,uFAAuF;AACvF,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,wCAAwC;AACxC,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Identity domain errors (security hardening Phase 1). Thrown by identity services, mapped to HTTP
|
|
4
|
+
* by {@link IdentityErrorInterceptor} in ONE place so every identity route answers the same way.
|
|
5
|
+
*
|
|
6
|
+
* Messages are user-facing: they name no raw ids (ADR-0012) and carry no store detail.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.IdentityBadRequestError = exports.IdentityConflictError = exports.IdentityNotFoundError = exports.IdentityForbiddenError = void 0;
|
|
10
|
+
/** The caller may not perform this action on this target (→ 403). */
|
|
11
|
+
class IdentityForbiddenError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'IdentityForbiddenError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.IdentityForbiddenError = IdentityForbiddenError;
|
|
18
|
+
/** The target does not exist or is outside the caller's reach (→ 404, no existence leak). */
|
|
19
|
+
class IdentityNotFoundError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'IdentityNotFoundError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.IdentityNotFoundError = IdentityNotFoundError;
|
|
26
|
+
/** The request conflicts with the current state (cycle, children present, … → 409). */
|
|
27
|
+
class IdentityConflictError extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'IdentityConflictError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.IdentityConflictError = IdentityConflictError;
|
|
34
|
+
/** The request is malformed (→ 400). */
|
|
35
|
+
class IdentityBadRequestError extends Error {
|
|
36
|
+
constructor(message) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = 'IdentityBadRequestError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.IdentityBadRequestError = IdentityBadRequestError;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot check (security hardening Phase 3 review P2). Disabling a user revokes its sessions through
|
|
3
|
+
* guard's `SHIELD_SESSION_SWEEPER` (provided by the auth surface). When the auth surface is on —
|
|
4
|
+
* guard's session token binder is visible — but no sweeper is, a disable still drops the user's
|
|
5
|
+
* roles (version bump + `[]` roles) but cannot end its sessions: say so at boot instead of failing
|
|
6
|
+
* silently. Auth off → no sessions exist → nothing to warn about.
|
|
7
|
+
*
|
|
8
|
+
* @returns `true` when the warning was emitted.
|
|
9
|
+
*/
|
|
10
|
+
export declare function warnIfSessionsUnrevocable(binder: unknown, sweeper: unknown, warn: (message: string) => void): boolean;
|
|
11
|
+
//# sourceMappingURL=session-revocation-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-revocation-check.d.ts","sourceRoot":"","sources":["../../../src/features/user/session-revocation-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,OAAO,CAQT"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.warnIfSessionsUnrevocable = warnIfSessionsUnrevocable;
|
|
4
|
+
/**
|
|
5
|
+
* Boot check (security hardening Phase 3 review P2). Disabling a user revokes its sessions through
|
|
6
|
+
* guard's `SHIELD_SESSION_SWEEPER` (provided by the auth surface). When the auth surface is on —
|
|
7
|
+
* guard's session token binder is visible — but no sweeper is, a disable still drops the user's
|
|
8
|
+
* roles (version bump + `[]` roles) but cannot end its sessions: say so at boot instead of failing
|
|
9
|
+
* silently. Auth off → no sessions exist → nothing to warn about.
|
|
10
|
+
*
|
|
11
|
+
* @returns `true` when the warning was emitted.
|
|
12
|
+
*/
|
|
13
|
+
function warnIfSessionsUnrevocable(binder, sweeper, warn) {
|
|
14
|
+
if (binder === undefined || binder === null)
|
|
15
|
+
return false;
|
|
16
|
+
if (sweeper !== undefined && sweeper !== null)
|
|
17
|
+
return false;
|
|
18
|
+
warn('SHIELD_SESSION_SWEEPER is not available: disabling a user will not revoke its sessions ' +
|
|
19
|
+
'(its roles still drop on the next request). Provide a sessionSweeper to ShieldModule.forRoot.');
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { UserService } from './user.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 UserController {
|
|
4
5
|
private readonly users;
|
|
5
6
|
constructor(users: UserService);
|
|
@@ -14,25 +15,29 @@ export declare class UserController {
|
|
|
14
15
|
page: number;
|
|
15
16
|
limit: number;
|
|
16
17
|
}>;
|
|
17
|
-
/**
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Create a user — gated on `identity:user:create`. Optional `companyUuid` creates the user's
|
|
20
|
+
* first (active) membership in the same transaction; it must be a company the caller manages.
|
|
21
|
+
*/
|
|
22
|
+
createUser(req: AuthedRequest, body: {
|
|
19
23
|
username?: string;
|
|
24
|
+
companyUuid?: string;
|
|
20
25
|
}): Promise<{
|
|
21
26
|
id: import("packages/engine/dist/types.js").RecordId;
|
|
22
27
|
}>;
|
|
23
28
|
/** Read one user — gated on `identity:user:read` (CONTROL-plane; root, the identity
|
|
24
29
|
* administrator, may read users). */
|
|
25
30
|
getUser(uuid: string): Promise<import("packages/engine/dist/types.js").RecordView | null>;
|
|
26
|
-
/** Update a user — gated on `identity:user:update`. */
|
|
27
|
-
updateUser(uuid: string, body: {
|
|
31
|
+
/** Update a user — gated on `identity:user:update`. Only `username`, `email`, `status` apply. */
|
|
32
|
+
updateUser(req: AuthedRequest, uuid: string, body: {
|
|
28
33
|
username?: string;
|
|
29
34
|
email?: string;
|
|
30
35
|
status?: string;
|
|
31
36
|
}): Promise<{
|
|
32
37
|
success: boolean;
|
|
33
38
|
}>;
|
|
34
|
-
/** Delete a user — gated on `identity:user:delete
|
|
35
|
-
deleteUser(uuid: string): Promise<{
|
|
39
|
+
/** Delete a user — gated on `identity:user:delete`; the caller must dominate the user. */
|
|
40
|
+
deleteUser(req: AuthedRequest, uuid: string): Promise<{
|
|
36
41
|
success: boolean;
|
|
37
42
|
}>;
|
|
38
43
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.controller.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"user.controller.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C;;;OAGG;IAGG,UAAU,CACP,GAAG,EAAE,aAAa,EACjB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAW3D;0CACsC;IAGtC,OAAO,CAAgB,IAAI,EAAE,MAAM;IAInC,iGAAiG;IAG3F,UAAU,CACP,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAUtE,0FAA0F;IAGpF,UAAU,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;CAIxE"}
|
|
@@ -17,9 +17,11 @@ const common_1 = require("@nestjs/common");
|
|
|
17
17
|
const guard_1 = require("@astram/guard");
|
|
18
18
|
const user_service_js_1 = require("./user.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
|
// User management is gated on `identity:user:<action>` grants. `user` is CONTROL-plane
|
|
21
22
|
// for ALL actions — including read: the capped root is the identity administrator (the
|
|
22
|
-
// identity-administrator model), so it may list/read users AND manage them.
|
|
23
|
+
// identity-administrator model), so it may list/read users AND manage them. Changing a user's
|
|
24
|
+
// email or status, or deleting them, also needs the caller to dominate that user (D2).
|
|
23
25
|
let UserController = class UserController {
|
|
24
26
|
users;
|
|
25
27
|
constructor(users) {
|
|
@@ -35,9 +37,17 @@ let UserController = class UserController {
|
|
|
35
37
|
const result = await this.users.listUsers(params);
|
|
36
38
|
return { total: result.total, rows: result.rows, page, limit };
|
|
37
39
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Create a user — gated on `identity:user:create`. Optional `companyUuid` creates the user's
|
|
42
|
+
* first (active) membership in the same transaction; it must be a company the caller manages.
|
|
43
|
+
*/
|
|
44
|
+
async createUser(req, body) {
|
|
45
|
+
const id = await this.users.createUser(req.principal, {
|
|
46
|
+
username: body?.username ?? 'Untitled',
|
|
47
|
+
...(typeof body?.companyUuid === 'string' && body.companyUuid.length > 0
|
|
48
|
+
? { companyUuid: body.companyUuid }
|
|
49
|
+
: {}),
|
|
50
|
+
});
|
|
41
51
|
return { id };
|
|
42
52
|
}
|
|
43
53
|
/** Read one user — gated on `identity:user:read` (CONTROL-plane; root, the identity
|
|
@@ -45,14 +55,18 @@ let UserController = class UserController {
|
|
|
45
55
|
getUser(uuid) {
|
|
46
56
|
return this.users.getUser(uuid);
|
|
47
57
|
}
|
|
48
|
-
/** Update a user — gated on `identity:user:update`. */
|
|
49
|
-
async updateUser(uuid, body) {
|
|
50
|
-
await this.users.updateUser(uuid,
|
|
58
|
+
/** Update a user — gated on `identity:user:update`. Only `username`, `email`, `status` apply. */
|
|
59
|
+
async updateUser(req, uuid, body) {
|
|
60
|
+
await this.users.updateUser(req.principal, uuid, {
|
|
61
|
+
username: body?.username,
|
|
62
|
+
email: body?.email,
|
|
63
|
+
status: body?.status,
|
|
64
|
+
});
|
|
51
65
|
return { success: true };
|
|
52
66
|
}
|
|
53
|
-
/** Delete a user — gated on `identity:user:delete
|
|
54
|
-
async deleteUser(uuid) {
|
|
55
|
-
await this.users.deleteUser(uuid);
|
|
67
|
+
/** Delete a user — gated on `identity:user:delete`; the caller must dominate the user. */
|
|
68
|
+
async deleteUser(req, uuid) {
|
|
69
|
+
await this.users.deleteUser(req.principal, uuid);
|
|
56
70
|
return { success: true };
|
|
57
71
|
}
|
|
58
72
|
};
|
|
@@ -68,9 +82,10 @@ __decorate([
|
|
|
68
82
|
__decorate([
|
|
69
83
|
(0, common_1.Post)('users'),
|
|
70
84
|
(0, guard_1.Classified)({ typeKey: 'user', action: 'create' }),
|
|
71
|
-
__param(0, (0, common_1.
|
|
85
|
+
__param(0, (0, common_1.Req)()),
|
|
86
|
+
__param(1, (0, common_1.Body)()),
|
|
72
87
|
__metadata("design:type", Function),
|
|
73
|
-
__metadata("design:paramtypes", [Object]),
|
|
88
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
74
89
|
__metadata("design:returntype", Promise)
|
|
75
90
|
], UserController.prototype, "createUser", null);
|
|
76
91
|
__decorate([
|
|
@@ -84,21 +99,24 @@ __decorate([
|
|
|
84
99
|
__decorate([
|
|
85
100
|
(0, common_1.Put)('users/:uuid'),
|
|
86
101
|
(0, guard_1.Classified)({ typeKey: 'user', action: 'update' }),
|
|
87
|
-
__param(0, (0, common_1.
|
|
88
|
-
__param(1, (0, common_1.
|
|
102
|
+
__param(0, (0, common_1.Req)()),
|
|
103
|
+
__param(1, (0, common_1.Param)('uuid')),
|
|
104
|
+
__param(2, (0, common_1.Body)()),
|
|
89
105
|
__metadata("design:type", Function),
|
|
90
|
-
__metadata("design:paramtypes", [String, Object]),
|
|
106
|
+
__metadata("design:paramtypes", [Object, String, Object]),
|
|
91
107
|
__metadata("design:returntype", Promise)
|
|
92
108
|
], UserController.prototype, "updateUser", null);
|
|
93
109
|
__decorate([
|
|
94
110
|
(0, common_1.Delete)('users/:uuid'),
|
|
95
111
|
(0, guard_1.Classified)({ typeKey: 'user', action: 'delete' }),
|
|
96
|
-
__param(0, (0, common_1.
|
|
112
|
+
__param(0, (0, common_1.Req)()),
|
|
113
|
+
__param(1, (0, common_1.Param)('uuid')),
|
|
97
114
|
__metadata("design:type", Function),
|
|
98
|
-
__metadata("design:paramtypes", [String]),
|
|
115
|
+
__metadata("design:paramtypes", [Object, String]),
|
|
99
116
|
__metadata("design:returntype", Promise)
|
|
100
117
|
], UserController.prototype, "deleteUser", null);
|
|
101
118
|
exports.UserController = UserController = __decorate([
|
|
102
119
|
(0, common_1.Controller)('identity'),
|
|
120
|
+
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
103
121
|
__metadata("design:paramtypes", [user_service_js_1.UserService])
|
|
104
122
|
], UserController);
|