@astram/identity 0.15.0 → 0.17.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/branch.object.d.ts +9 -1
- package/dist/branch.object.d.ts.map +1 -1
- package/dist/branch.object.js +14 -1
- package/dist/company-branch.object.d.ts +5 -4
- package/dist/company-branch.object.d.ts.map +1 -1
- package/dist/company-branch.object.js +6 -4
- package/dist/company-department.object.d.ts +6 -6
- package/dist/company-department.object.d.ts.map +1 -1
- package/dist/company-department.object.js +7 -6
- package/dist/department.object.d.ts +9 -1
- package/dist/department.object.d.ts.map +1 -1
- package/dist/department.object.js +14 -1
- package/dist/features/access/access-admin.access.d.ts.map +1 -1
- package/dist/features/access/access-admin.access.js +8 -1
- package/dist/features/access/index.d.ts +1 -0
- package/dist/features/access/index.d.ts.map +1 -1
- package/dist/features/access/index.js +10 -2
- package/dist/features/access/org-structure.access.d.ts +3 -0
- package/dist/features/access/org-structure.access.d.ts.map +1 -0
- package/dist/features/access/org-structure.access.js +41 -0
- package/dist/features/access/user-management.access.d.ts.map +1 -1
- package/dist/features/access/user-management.access.js +5 -0
- package/dist/features/audit/audit-read.service.d.ts +32 -1
- package/dist/features/audit/audit-read.service.d.ts.map +1 -1
- package/dist/features/audit/audit-read.service.js +67 -14
- package/dist/features/audit/auth-audit-sink.d.ts +19 -0
- package/dist/features/audit/auth-audit-sink.d.ts.map +1 -0
- package/dist/features/audit/auth-audit-sink.js +24 -0
- package/dist/features/audit/directory-label-resolver.d.ts +10 -1
- package/dist/features/audit/directory-label-resolver.d.ts.map +1 -1
- package/dist/features/audit/directory-label-resolver.js +28 -1
- package/dist/features/authz/grant-admin.controller.d.ts +4 -4
- package/dist/features/authz/grant-admin.controller.js +4 -4
- package/dist/features/authz/grant-admin.service.d.ts +9 -8
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +20 -24
- package/dist/features/authz/grant-ceiling.d.ts +35 -16
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +44 -29
- package/dist/features/authz/role-binding.d.ts +25 -24
- package/dist/features/authz/role-binding.d.ts.map +1 -1
- package/dist/features/authz/role-binding.js +31 -44
- package/dist/features/identity-feature.module.d.ts +0 -11
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +90 -1
- package/dist/features/membership/membership.controller.d.ts.map +1 -1
- package/dist/features/membership/membership.controller.js +20 -6
- package/dist/features/membership/membership.service.d.ts +16 -10
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +61 -21
- package/dist/features/membership/posting.service.d.ts +25 -6
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +63 -16
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +6 -2
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-bootstrap-file.js +54 -11
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +30 -11
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-bootstrap-plan.js +172 -68
- package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-state.reader.js +56 -49
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +6 -1
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -1
- package/dist/features/org-bootstrap/run-org-bootstrap.js +81 -92
- package/dist/features/org-structure/org-catalog.controller.d.ts +29 -6
- package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.controller.js +105 -17
- package/dist/features/org-structure/org-catalog.service.d.ts +99 -18
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.service.js +327 -51
- package/dist/features/org-structure/org-unit-name.d.ts +31 -0
- package/dist/features/org-structure/org-unit-name.d.ts.map +1 -0
- package/dist/features/org-structure/org-unit-name.js +50 -0
- package/dist/features/role/role.controller.d.ts +28 -13
- package/dist/features/role/role.controller.d.ts.map +1 -1
- package/dist/features/role/role.controller.js +54 -14
- package/dist/features/role/role.service.d.ts +76 -21
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +204 -27
- package/dist/features/security-policy/security-policy-version-bump.d.ts +41 -0
- package/dist/features/security-policy/security-policy-version-bump.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy-version-bump.js +65 -0
- package/dist/features/security-policy/security-policy.controller.d.ts +51 -0
- package/dist/features/security-policy/security-policy.controller.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy.controller.js +121 -0
- package/dist/features/security-policy/security-policy.service.d.ts +94 -0
- package/dist/features/security-policy/security-policy.service.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy.service.js +429 -0
- package/dist/features/service-account/service-account.service.d.ts +13 -3
- package/dist/features/service-account/service-account.service.d.ts.map +1 -1
- package/dist/features/service-account/service-account.service.js +48 -10
- package/dist/features/service-account/service-account.store.d.ts +11 -1
- package/dist/features/service-account/service-account.store.d.ts.map +1 -1
- package/dist/features/service-account/service-account.store.js +20 -1
- package/dist/features/user/signin-admin.tokens.d.ts +11 -0
- package/dist/features/user/signin-admin.tokens.d.ts.map +1 -0
- package/dist/features/user/signin-admin.tokens.js +13 -0
- package/dist/features/user/user-mfa-status.d.ts +38 -0
- package/dist/features/user/user-mfa-status.d.ts.map +1 -0
- package/dist/features/user/user-mfa-status.js +47 -0
- package/dist/features/user/user-signin-admin.service.d.ts +48 -0
- package/dist/features/user/user-signin-admin.service.d.ts.map +1 -0
- package/dist/features/user/user-signin-admin.service.js +232 -0
- package/dist/features/user/user.controller.d.ts +29 -2
- package/dist/features/user/user.controller.d.ts.map +1 -1
- package/dist/features/user/user.controller.js +71 -3
- package/dist/features/user/user.service.d.ts +40 -14
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +143 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/objects.d.ts.map +1 -1
- package/dist/objects.js +2 -0
- package/dist/role.object.d.ts +1 -1
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +6 -15
- package/dist/security-policy.object.d.ts +43 -0
- package/dist/security-policy.object.d.ts.map +1 -0
- package/dist/security-policy.object.js +107 -0
- package/dist/user.object.d.ts +6 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +13 -1
- package/migrations/1710000000102-CreateAuditChain.cjs +71 -0
- package/migrations/1710000000103-AddUserLoginName.cjs +87 -0
- package/migrations/1710000000104-CreateSecurityPolicy.cjs +90 -0
- package/migrations/1710000000105-OwnDepartmentsAndBranches.cjs +327 -0
- package/migrations/1710000000106-OwnRolesPerCompany.cjs +622 -0
- package/package.json +9 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.controller.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.controller.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACV,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"user.controller.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.controller.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,KAAK,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAC9F,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACV,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;gBAFxB,KAAK,EAAE,WAAW,EACN,OAAO,CAAC,EAAE,aAAa,YAAA,EACvB,WAAW,CAAC,EAAE,sBAAsB,YAAA;IAGnE;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C;;;OAGG;IAGG,UAAU,CACP,GAAG,EAAE,aAAa,EAEzB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IAc9F;0CACsC;IAGtC,OAAO,CAA0B,IAAI,EAAE,MAAM;IAI7C;;;OAGG;IAGG,UAAU,CACP,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAErC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;IAWzF;;;;;OAKG;IAIH,UAAU,CACD,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,GACpC,OAAO,CAAC,YAAY,CAAC;IAKxB;;;;;;;OAOG;IAIH,YAAY,CACH,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE,eAAe,GAC5B,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC;IAI7B,2FAA2F;IAI3F,UAAU,CACD,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE,eAAe,GAC5B,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC;IAI7B,qFAAqF;IAIrF,eAAe,CACN,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE,eAAe,GAC5B,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC;IAI7B,OAAO,CAAC,kBAAkB;IAK1B,0FAA0F;IAGpF,UAAU,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIlF"}
|
|
@@ -17,6 +17,7 @@ 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 invite_service_js_1 = require("./invite.service.js");
|
|
20
|
+
const user_signin_admin_service_js_1 = require("./user-signin-admin.service.js");
|
|
20
21
|
const list_query_js_1 = require("../shared/list-query.js");
|
|
21
22
|
const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
|
|
22
23
|
// User management is gated on `identity:user:<action>` grants. `user` is CONTROL-plane
|
|
@@ -26,9 +27,11 @@ const identity_error_interceptor_js_1 = require("../shared/identity-error.interc
|
|
|
26
27
|
let UserController = class UserController {
|
|
27
28
|
users;
|
|
28
29
|
invites;
|
|
29
|
-
|
|
30
|
+
signInAdmin;
|
|
31
|
+
constructor(users, invites, signInAdmin) {
|
|
30
32
|
this.users = users;
|
|
31
33
|
this.invites = invites;
|
|
34
|
+
this.signInAdmin = signInAdmin;
|
|
32
35
|
}
|
|
33
36
|
/**
|
|
34
37
|
* List users — gated on `identity:user:read` (see class doc). Server-side DataTable:
|
|
@@ -51,6 +54,8 @@ let UserController = class UserController {
|
|
|
51
54
|
// KI #94: the template form sends it; the service canonicalises and validates it.
|
|
52
55
|
...(typeof body?.email === 'string' ? { email: body.email } : {}),
|
|
53
56
|
...(companyUuid !== undefined ? { companyUuid } : {}),
|
|
57
|
+
// R1.7: optional login name (validated by the service; admin-only route).
|
|
58
|
+
...(body?.loginName !== undefined ? { loginName: body.loginName } : {}),
|
|
54
59
|
});
|
|
55
60
|
return result;
|
|
56
61
|
}
|
|
@@ -59,12 +64,16 @@ let UserController = class UserController {
|
|
|
59
64
|
getUser(uuid) {
|
|
60
65
|
return this.users.getUser(uuid);
|
|
61
66
|
}
|
|
62
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Update a user — gated on `identity:user:update`. Only `username`, `email`, `status` and
|
|
69
|
+
* `loginName` (R1.7; `null` clears; needs dominance) apply.
|
|
70
|
+
*/
|
|
63
71
|
async updateUser(req, uuid, body) {
|
|
64
72
|
await this.users.updateUser(req.principal, uuid, {
|
|
65
73
|
username: body?.username,
|
|
66
74
|
email: body?.email,
|
|
67
75
|
status: body?.status,
|
|
76
|
+
loginName: body?.loginName,
|
|
68
77
|
});
|
|
69
78
|
return { success: true };
|
|
70
79
|
}
|
|
@@ -79,6 +88,30 @@ let UserController = class UserController {
|
|
|
79
88
|
throw new Error('UserController: InviteService is not wired');
|
|
80
89
|
return this.invites.invite(req.principal, uuid);
|
|
81
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Auth capabilities R11 (task 18): admin sign-in controls, each a custom action on `user`
|
|
93
|
+
* (ADR-0038) with the admin's own TOTP in the body `{ code }`, record-level check and dominance in
|
|
94
|
+
* the service. 200 `{ success: true }`; 400 `{error:'mfa_required'}` no code, 400
|
|
95
|
+
* `{error:'invalid_request'}` wrong code, 409 `{error:'mfa_required'}` admin has no 2FA, 403 not
|
|
96
|
+
* allowed (yourself, no grant, other tenant, not dominated), 429 step-up budget, 503 unavailable
|
|
97
|
+
* (`{error:'partially_applied', message}` when the action was applied but a later step failed).
|
|
98
|
+
*/
|
|
99
|
+
resetUserMfa(req, uuid, body) {
|
|
100
|
+
return this.signInAdminService().resetMfa(req.principal, uuid, body);
|
|
101
|
+
}
|
|
102
|
+
/** R11: clear the user's sign-in lockout counters — `identity:user:unlock` (see above). */
|
|
103
|
+
unlockUser(req, uuid, body) {
|
|
104
|
+
return this.signInAdminService().unlock(req.principal, uuid, body);
|
|
105
|
+
}
|
|
106
|
+
/** R11: end every session of the user — `identity:user:end_sessions` (see above). */
|
|
107
|
+
endUserSessions(req, uuid, body) {
|
|
108
|
+
return this.signInAdminService().endSessions(req.principal, uuid, body);
|
|
109
|
+
}
|
|
110
|
+
signInAdminService() {
|
|
111
|
+
if (!this.signInAdmin)
|
|
112
|
+
throw new Error('UserController: UserSignInAdminService is not wired');
|
|
113
|
+
return this.signInAdmin;
|
|
114
|
+
}
|
|
82
115
|
/** Delete a user — gated on `identity:user:delete`; the caller must dominate the user. */
|
|
83
116
|
async deleteUser(req, uuid) {
|
|
84
117
|
await this.users.deleteUser(req.principal, uuid);
|
|
@@ -131,6 +164,39 @@ __decorate([
|
|
|
131
164
|
__metadata("design:paramtypes", [Object, String]),
|
|
132
165
|
__metadata("design:returntype", Promise)
|
|
133
166
|
], UserController.prototype, "inviteUser", null);
|
|
167
|
+
__decorate([
|
|
168
|
+
(0, common_1.Post)('users/:uuid/reset-mfa'),
|
|
169
|
+
(0, common_1.HttpCode)(common_1.HttpStatus.OK),
|
|
170
|
+
(0, guard_1.Classified)({ typeKey: 'user', action: 'reset_mfa' }),
|
|
171
|
+
__param(0, (0, common_1.Req)()),
|
|
172
|
+
__param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
|
|
173
|
+
__param(2, (0, common_1.Body)()),
|
|
174
|
+
__metadata("design:type", Function),
|
|
175
|
+
__metadata("design:paramtypes", [Object, String, Object]),
|
|
176
|
+
__metadata("design:returntype", Promise)
|
|
177
|
+
], UserController.prototype, "resetUserMfa", null);
|
|
178
|
+
__decorate([
|
|
179
|
+
(0, common_1.Post)('users/:uuid/unlock'),
|
|
180
|
+
(0, common_1.HttpCode)(common_1.HttpStatus.OK),
|
|
181
|
+
(0, guard_1.Classified)({ typeKey: 'user', action: 'unlock' }),
|
|
182
|
+
__param(0, (0, common_1.Req)()),
|
|
183
|
+
__param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
|
|
184
|
+
__param(2, (0, common_1.Body)()),
|
|
185
|
+
__metadata("design:type", Function),
|
|
186
|
+
__metadata("design:paramtypes", [Object, String, Object]),
|
|
187
|
+
__metadata("design:returntype", Promise)
|
|
188
|
+
], UserController.prototype, "unlockUser", null);
|
|
189
|
+
__decorate([
|
|
190
|
+
(0, common_1.Post)('users/:uuid/end-sessions'),
|
|
191
|
+
(0, common_1.HttpCode)(common_1.HttpStatus.OK),
|
|
192
|
+
(0, guard_1.Classified)({ typeKey: 'user', action: 'end_sessions' }),
|
|
193
|
+
__param(0, (0, common_1.Req)()),
|
|
194
|
+
__param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
|
|
195
|
+
__param(2, (0, common_1.Body)()),
|
|
196
|
+
__metadata("design:type", Function),
|
|
197
|
+
__metadata("design:paramtypes", [Object, String, Object]),
|
|
198
|
+
__metadata("design:returntype", Promise)
|
|
199
|
+
], UserController.prototype, "endUserSessions", null);
|
|
134
200
|
__decorate([
|
|
135
201
|
(0, common_1.Delete)('users/:uuid'),
|
|
136
202
|
(0, guard_1.Classified)({ typeKey: 'user', action: 'delete' }),
|
|
@@ -144,6 +210,8 @@ exports.UserController = UserController = __decorate([
|
|
|
144
210
|
(0, common_1.Controller)('identity'),
|
|
145
211
|
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
146
212
|
__param(1, (0, common_1.Optional)()),
|
|
213
|
+
__param(2, (0, common_1.Optional)()),
|
|
147
214
|
__metadata("design:paramtypes", [user_service_js_1.UserService,
|
|
148
|
-
invite_service_js_1.InviteService
|
|
215
|
+
invite_service_js_1.InviteService,
|
|
216
|
+
user_signin_admin_service_js_1.UserSignInAdminService])
|
|
149
217
|
], UserController);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { RecordsService, type RecordId, type RecordView } from '@astram/engine';
|
|
2
|
-
import type
|
|
2
|
+
import { type FrameworkSchemas, type Principal } from '@astram/core';
|
|
3
3
|
import { type EmailAddressChanger, type CredentialProvisioner, type EmailSender, type MessageDispatcher, type ResetTokenStore, type SessionSweeper } from '@astram/guard';
|
|
4
4
|
import { type PostingVersionBump } from '../membership/posting-version-bump.js';
|
|
5
5
|
import { type ListParams } from '../shared/list-query.js';
|
|
6
6
|
import { GrantCeiling } from '../authz/grant-ceiling.js';
|
|
7
|
+
import type { UserMfaStatus, UserMfaStatusReader } from './user-mfa-status.js';
|
|
7
8
|
/** The outcome of {@link UserService.createUserWithCredential}. */
|
|
8
9
|
export type CreateUserResult = {
|
|
9
10
|
id: RecordId;
|
|
@@ -18,15 +19,29 @@ export type CreateUserResult = {
|
|
|
18
19
|
* locked → `pending` (invite-able), usable → `active`, none → `none`. Never any credential data.
|
|
19
20
|
*/
|
|
20
21
|
export type SignInStatus = 'pending' | 'active' | 'none';
|
|
21
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* A user record as `GET /identity/users[/:uuid]` returns it: the engine row plus `signInStatus` and
|
|
24
|
+
* (auth capabilities R11.3) `mfaStatus` — never device names, secrets or counts.
|
|
25
|
+
*/
|
|
22
26
|
export type UserRecord = RecordView & {
|
|
23
27
|
readonly signInStatus?: SignInStatus;
|
|
28
|
+
readonly mfaStatus?: UserMfaStatus;
|
|
24
29
|
};
|
|
25
30
|
/** The patch keys a generic user update accepts (security hardening #3). */
|
|
26
31
|
export interface UserPatch {
|
|
27
32
|
username?: unknown;
|
|
28
33
|
email?: unknown;
|
|
29
34
|
status?: unknown;
|
|
35
|
+
/** R1.7: the login name (`null` clears); admin-only, needs dominance. */
|
|
36
|
+
loginName?: unknown;
|
|
37
|
+
}
|
|
38
|
+
/** The input of {@link UserService.createUserWithCredential}. */
|
|
39
|
+
export interface CreateUserInput {
|
|
40
|
+
username: string;
|
|
41
|
+
email?: string | undefined;
|
|
42
|
+
companyUuid?: string | undefined;
|
|
43
|
+
/** R1.7: optional login name (normalized and validated; blank ⇒ none). */
|
|
44
|
+
loginName?: string | null | undefined;
|
|
30
45
|
}
|
|
31
46
|
/**
|
|
32
47
|
* UserService — user (principal) CRUD on the paved road, via the framework's RecordsService facade
|
|
@@ -57,6 +72,7 @@ export declare class UserService {
|
|
|
57
72
|
private readonly emailAddresses?;
|
|
58
73
|
private readonly provisioner?;
|
|
59
74
|
private readonly dispatcher?;
|
|
75
|
+
private readonly mfaStatus?;
|
|
60
76
|
private readonly logger;
|
|
61
77
|
/**
|
|
62
78
|
* The engine's audit sink, used on the email helper's transaction (no pool of its own), on the
|
|
@@ -64,16 +80,12 @@ export declare class UserService {
|
|
|
64
80
|
* service) ⇒ the sink resolves `SHIELD_DB_NAMESPACE` itself.
|
|
65
81
|
*/
|
|
66
82
|
private readonly emailAudit;
|
|
67
|
-
constructor(records: RecordsService, ceiling: GrantCeiling, resetTokens?: (ResetTokenStore | null) | undefined, emailSender?: (EmailSender | null) | undefined, sessions?: (SessionSweeper | null) | undefined, versionBump?: (PostingVersionBump | null) | undefined, emailAddresses?: (EmailAddressChanger | null) | undefined, schemas?: FrameworkSchemas | null, provisioner?: (CredentialProvisioner | null) | undefined, dispatcher?: (MessageDispatcher | null) | undefined);
|
|
83
|
+
constructor(records: RecordsService, ceiling: GrantCeiling, resetTokens?: (ResetTokenStore | null) | undefined, emailSender?: (EmailSender | null) | undefined, sessions?: (SessionSweeper | null) | undefined, versionBump?: (PostingVersionBump | null) | undefined, emailAddresses?: (EmailAddressChanger | null) | undefined, schemas?: FrameworkSchemas | null, provisioner?: (CredentialProvisioner | null) | undefined, dispatcher?: (MessageDispatcher | null) | undefined, mfaStatus?: (UserMfaStatusReader | null) | undefined);
|
|
68
84
|
/**
|
|
69
85
|
* Create a user; returns its id. Delegates to {@link createUserWithCredential} (same behaviour;
|
|
70
86
|
* the detailed credential outcome is dropped to keep this public signature stable).
|
|
71
87
|
*/
|
|
72
|
-
createUser(actor: Principal, input:
|
|
73
|
-
username: string;
|
|
74
|
-
email?: string | undefined;
|
|
75
|
-
companyUuid?: string | undefined;
|
|
76
|
-
}): Promise<RecordId>;
|
|
88
|
+
createUser(actor: Principal, input: CreateUserInput): Promise<RecordId>;
|
|
77
89
|
/**
|
|
78
90
|
* Create a user. With `companyUuid`, the user is created AND attached (one active membership) in
|
|
79
91
|
* ONE transaction — the onboarding path for a non-root admin, whose membership create is checked
|
|
@@ -89,11 +101,7 @@ export declare class UserService {
|
|
|
89
101
|
* transaction's own connection; a provisioner failure rolls everything back. Otherwise no
|
|
90
102
|
* credential is created and the result says why (no email takes precedence).
|
|
91
103
|
*/
|
|
92
|
-
createUserWithCredential(actor: Principal, input:
|
|
93
|
-
username: string;
|
|
94
|
-
email?: string | undefined;
|
|
95
|
-
companyUuid?: string | undefined;
|
|
96
|
-
}): Promise<CreateUserResult>;
|
|
104
|
+
createUserWithCredential(actor: Principal, input: CreateUserInput): Promise<CreateUserResult>;
|
|
97
105
|
/**
|
|
98
106
|
* List users (tenant-scoped by the pipe), with paging/search/sort. Each row gains a top-level
|
|
99
107
|
* `signInStatus` (T10), from ONE batched credential read for the page.
|
|
@@ -104,10 +112,23 @@ export declare class UserService {
|
|
|
104
112
|
}>;
|
|
105
113
|
/** Read a single user, with its top-level `signInStatus` (T10). */
|
|
106
114
|
getUser(uuid: string): Promise<UserRecord | null>;
|
|
107
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* One `statesOf` call for the given rows (no provisioner, a hand-built service ⇒ no
|
|
117
|
+
* `signInStatus`), and one batched `mfaStatus` read (R11.3; no reader ⇒ no `mfaStatus`).
|
|
118
|
+
*/
|
|
108
119
|
private withSignInStatus;
|
|
120
|
+
/** R11.3: the page's `mfaStatus`; a store error is a fixed 503, never a guessed value. */
|
|
121
|
+
private mfaStatusesOf;
|
|
109
122
|
/** Update a user (whitelisted patch; username/email/status changes need dominance — class doc). */
|
|
110
123
|
updateUser(actor: Principal, uuid: string, patch: UserPatch): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Review round 1, item F: a login name must not equal another principal's legacy sign-in hint
|
|
126
|
+
* (`identity_credential.ic_identity_hint`, compared under the column's case-insensitive
|
|
127
|
+
* collation); the sign-in lookup would then match two accounts. Same fixed 409 as a login-name
|
|
128
|
+
* clash (no new oracle). Read on the write's transaction through guard's credential provisioner;
|
|
129
|
+
* a hand-built service without one (or a custom provisioner without `hintHolderOf`) skips it.
|
|
130
|
+
*/
|
|
131
|
+
private assertLoginNameNotAHint;
|
|
111
132
|
/**
|
|
112
133
|
* Phase 12 (KI #14): write a dominated user's new (canonical) email through guard's
|
|
113
134
|
* email-change helper — ONE transaction that locks the user and credential rows, refuses a
|
|
@@ -116,6 +137,11 @@ export declare class UserService {
|
|
|
116
137
|
* (engine shape, fail closed). Reset links are burned BEFORE (a failure aborts the change).
|
|
117
138
|
*/
|
|
118
139
|
private changeEmail;
|
|
140
|
+
/**
|
|
141
|
+
* R1.7 / R12.2: the `auth.handle.changed` row (type `authentication`) on the write's own
|
|
142
|
+
* transaction. Actor and target only: no login name (old or new) is written.
|
|
143
|
+
*/
|
|
144
|
+
private auditHandleChanged;
|
|
119
145
|
/** The helper call with the audit row on its transaction (see {@link changeEmail}). */
|
|
120
146
|
private applyEmailChange;
|
|
121
147
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,cAAc,EAAE,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAElG,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,14 +17,17 @@ exports.UserService = void 0;
|
|
|
17
17
|
const common_1 = require("@nestjs/common");
|
|
18
18
|
const engine_1 = require("@astram/engine");
|
|
19
19
|
const internal_1 = require("@astram/engine/internal");
|
|
20
|
+
const core_1 = require("@astram/core");
|
|
20
21
|
const guard_1 = require("@astram/guard");
|
|
21
22
|
const posting_version_bump_js_1 = require("../membership/posting-version-bump.js");
|
|
22
23
|
const list_query_js_1 = require("../shared/list-query.js");
|
|
23
24
|
const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
|
|
25
|
+
const http_js_1 = require("../shared/http.js");
|
|
24
26
|
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
25
27
|
const email_address_changer_token_js_1 = require("./email-address-changer.token.js");
|
|
26
28
|
const credential_provisioner_token_js_1 = require("./credential-provisioner.token.js");
|
|
27
29
|
const framework_schemas_token_js_1 = require("../framework-schemas.token.js");
|
|
30
|
+
const signin_admin_tokens_js_1 = require("./signin-admin.tokens.js");
|
|
28
31
|
/**
|
|
29
32
|
* KI #87: `users` has two unique keys besides the PK, `IDX_users_username` (migration 001) and
|
|
30
33
|
* `uq_users_email` (021, `utf8mb4_unicode_ci`: folds case, accents, width, trailing spaces). The
|
|
@@ -39,12 +42,32 @@ function isDuplicateKey(err) {
|
|
|
39
42
|
return e.errno === 1062 || e.code === 'ER_DUP_ENTRY';
|
|
40
43
|
}
|
|
41
44
|
const USERNAME_OR_EMAIL_TAKEN = 'That username or email is already in use.';
|
|
45
|
+
/** Auth capabilities R1.10: the fixed 409 for a login-name clash (never the value, no tenant). */
|
|
46
|
+
const LOGIN_NAME_TAKEN = 'That sign-in name is already in use.';
|
|
47
|
+
/** R1.3: the fixed 400 for a login name outside the rule (never echoes the value). */
|
|
48
|
+
const LOGIN_NAME_INVALID = 'Enter a valid sign-in name: 3–64 characters, letters a–z, digits, dot, underscore or hyphen.';
|
|
49
|
+
/** Migration 103's unique index on `users.u_login_name`. */
|
|
50
|
+
const LOGIN_NAME_INDEX = 'uq_users_login_name';
|
|
51
|
+
/** The audit action for a login-name set / change / clear (R1.7, R12.2). */
|
|
52
|
+
const HANDLE_CHANGED = 'auth.handle.changed';
|
|
53
|
+
/**
|
|
54
|
+
* The duplicate key is the login-name index. The driver names the key in its message; only that
|
|
55
|
+
* name is matched (the message itself, which carries the value, is never echoed or logged).
|
|
56
|
+
*/
|
|
57
|
+
function isLoginNameDuplicate(err) {
|
|
58
|
+
if (!isDuplicateKey(err))
|
|
59
|
+
return false;
|
|
60
|
+
const e = err;
|
|
61
|
+
return [e.sqlMessage, e.message].some((m) => typeof m === 'string' && m.includes(LOGIN_NAME_INDEX));
|
|
62
|
+
}
|
|
42
63
|
/** Run a user write; a duplicate key becomes {@link IdentityConflictError}, all else rethrows. */
|
|
43
64
|
async function mappingDuplicates(write) {
|
|
44
65
|
try {
|
|
45
66
|
return await write();
|
|
46
67
|
}
|
|
47
68
|
catch (err) {
|
|
69
|
+
if (isLoginNameDuplicate(err))
|
|
70
|
+
throw new identity_errors_js_1.IdentityConflictError(LOGIN_NAME_TAKEN);
|
|
48
71
|
// R1-4: an orphan credential already holds this login hint (deleteUser leaves it): same fixed
|
|
49
72
|
// 409 as a duplicate email, never the hint.
|
|
50
73
|
if (isDuplicateKey(err) || err instanceof guard_1.CredentialExistsError) {
|
|
@@ -53,6 +76,25 @@ async function mappingDuplicates(write) {
|
|
|
53
76
|
throw err;
|
|
54
77
|
}
|
|
55
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* R1.3 / R1.10: the requested login name. `undefined` ⇒ not part of the request; `null` (or a
|
|
81
|
+
* blank string) ⇒ clear; otherwise the normalized (trim + lower-case) value, which must pass core
|
|
82
|
+
* `isValidLoginName` (else a fixed 400, before any write).
|
|
83
|
+
*/
|
|
84
|
+
function requestedLoginName(value) {
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
return undefined;
|
|
87
|
+
if (value === null)
|
|
88
|
+
return null;
|
|
89
|
+
if (typeof value !== 'string')
|
|
90
|
+
throw new identity_errors_js_1.IdentityBadRequestError(LOGIN_NAME_INVALID);
|
|
91
|
+
const name = (0, core_1.normalizeLoginName)(value);
|
|
92
|
+
if (name === '')
|
|
93
|
+
return null;
|
|
94
|
+
if (!(0, core_1.isValidLoginName)(name))
|
|
95
|
+
throw new identity_errors_js_1.IdentityBadRequestError(LOGIN_NAME_INVALID);
|
|
96
|
+
return name;
|
|
97
|
+
}
|
|
56
98
|
const SIGN_IN_STATUS = {
|
|
57
99
|
locked: 'pending',
|
|
58
100
|
usable: 'active',
|
|
@@ -89,6 +131,7 @@ let UserService = UserService_1 = class UserService {
|
|
|
89
131
|
emailAddresses;
|
|
90
132
|
provisioner;
|
|
91
133
|
dispatcher;
|
|
134
|
+
mfaStatus;
|
|
92
135
|
logger = new common_1.Logger(UserService_1.name);
|
|
93
136
|
/**
|
|
94
137
|
* The engine's audit sink, used on the email helper's transaction (no pool of its own), on the
|
|
@@ -96,7 +139,7 @@ let UserService = UserService_1 = class UserService {
|
|
|
96
139
|
* service) ⇒ the sink resolves `SHIELD_DB_NAMESPACE` itself.
|
|
97
140
|
*/
|
|
98
141
|
emailAudit;
|
|
99
|
-
constructor(records, ceiling, resetTokens, emailSender, sessions, versionBump, emailAddresses, schemas, provisioner, dispatcher) {
|
|
142
|
+
constructor(records, ceiling, resetTokens, emailSender, sessions, versionBump, emailAddresses, schemas, provisioner, dispatcher, mfaStatus) {
|
|
100
143
|
this.records = records;
|
|
101
144
|
this.ceiling = ceiling;
|
|
102
145
|
this.resetTokens = resetTokens;
|
|
@@ -106,6 +149,7 @@ let UserService = UserService_1 = class UserService {
|
|
|
106
149
|
this.emailAddresses = emailAddresses;
|
|
107
150
|
this.provisioner = provisioner;
|
|
108
151
|
this.dispatcher = dispatcher;
|
|
152
|
+
this.mfaStatus = mfaStatus;
|
|
109
153
|
this.emailAudit = new engine_1.MariaDbAuditSink(schemas ? { schema: schemas.audit } : {});
|
|
110
154
|
}
|
|
111
155
|
/**
|
|
@@ -138,9 +182,20 @@ let UserService = UserService_1 = class UserService {
|
|
|
138
182
|
throw new identity_errors_js_1.IdentityBadRequestError('Enter a valid email address.');
|
|
139
183
|
fields.email = email;
|
|
140
184
|
}
|
|
185
|
+
// R1.7: an optional login name, written with the user and audited on the same transaction.
|
|
186
|
+
const loginName = requestedLoginName(input.loginName) ?? null;
|
|
187
|
+
if (loginName !== null)
|
|
188
|
+
fields.loginName = loginName;
|
|
141
189
|
const companyUuid = input.companyUuid;
|
|
142
190
|
if (!companyUuid) {
|
|
143
|
-
const id = await mappingDuplicates(() =>
|
|
191
|
+
const id = await mappingDuplicates(() => loginName === null
|
|
192
|
+
? this.records.create('user', { fields })
|
|
193
|
+
: this.records.transaction(async (tx) => {
|
|
194
|
+
const created = await tx.create('user', { fields });
|
|
195
|
+
await this.assertLoginNameNotAHint(tx, loginName, String(created));
|
|
196
|
+
await this.auditHandleChanged(tx, actor, String(created), false);
|
|
197
|
+
return created;
|
|
198
|
+
}));
|
|
144
199
|
return { id, credential: 'none', reason: email === '' ? 'no_email' : 'no_company' };
|
|
145
200
|
}
|
|
146
201
|
const provisioner = this.provisioner;
|
|
@@ -150,6 +205,10 @@ let UserService = UserService_1 = class UserService {
|
|
|
150
205
|
await tx.create('company_membership', {
|
|
151
206
|
fields: { userUuid: created, companyUuid, isActive: true, status: 'active' },
|
|
152
207
|
});
|
|
208
|
+
if (loginName !== null) {
|
|
209
|
+
await this.assertLoginNameNotAHint(tx, loginName, String(created));
|
|
210
|
+
await this.auditHandleChanged(tx, actor, String(created), false);
|
|
211
|
+
}
|
|
153
212
|
if (provision) {
|
|
154
213
|
await provisioner.provisionLocked({ principalId: created, hint: email, tenantId: companyUuid }, (0, internal_1.transactionQueryOf)(tx));
|
|
155
214
|
}
|
|
@@ -178,17 +237,38 @@ let UserService = UserService_1 = class UserService {
|
|
|
178
237
|
return null;
|
|
179
238
|
return (await this.withSignInStatus([record]))[0];
|
|
180
239
|
}
|
|
181
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* One `statesOf` call for the given rows (no provisioner, a hand-built service ⇒ no
|
|
242
|
+
* `signInStatus`), and one batched `mfaStatus` read (R11.3; no reader ⇒ no `mfaStatus`).
|
|
243
|
+
*/
|
|
182
244
|
async withSignInStatus(rows) {
|
|
183
|
-
|
|
184
|
-
if (rows.length === 0 || provisioner == null)
|
|
245
|
+
if (rows.length === 0)
|
|
185
246
|
return [...rows];
|
|
186
|
-
const
|
|
247
|
+
const ids = rows.map((r) => String(r.id));
|
|
248
|
+
const provisioner = this.provisioner;
|
|
249
|
+
const states = provisioner != null ? await provisioner.statesOf(ids) : null;
|
|
250
|
+
const mfa = await this.mfaStatusesOf(ids);
|
|
187
251
|
return rows.map((r) => ({
|
|
188
252
|
...r,
|
|
189
|
-
|
|
253
|
+
...(states !== null
|
|
254
|
+
? { signInStatus: SIGN_IN_STATUS[states.get(String(r.id)) ?? 'none'] }
|
|
255
|
+
: {}),
|
|
256
|
+
...(mfa !== null ? { mfaStatus: mfa.get(String(r.id)) ?? 'not_required' } : {}),
|
|
190
257
|
}));
|
|
191
258
|
}
|
|
259
|
+
/** R11.3: the page's `mfaStatus`; a store error is a fixed 503, never a guessed value. */
|
|
260
|
+
async mfaStatusesOf(ids) {
|
|
261
|
+
const reader = this.mfaStatus;
|
|
262
|
+
if (reader == null)
|
|
263
|
+
return null;
|
|
264
|
+
try {
|
|
265
|
+
return await reader.statusesOf(ids);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
this.logger.warn('user mfaStatus could not be read (store error)');
|
|
269
|
+
throw new identity_errors_js_1.IdentityUnavailableError('Sign-in status is temporarily unavailable. Try again.');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
192
272
|
/** Update a user (whitelisted patch; username/email/status changes need dominance — class doc). */
|
|
193
273
|
async updateUser(actor, uuid, patch) {
|
|
194
274
|
const current = await this.records.get('user', uuid);
|
|
@@ -214,13 +294,31 @@ let UserService = UserService_1 = class UserService {
|
|
|
214
294
|
const wantsInactive = typeof patch?.status === 'string' && patch.status !== 'active';
|
|
215
295
|
if (statusChanged)
|
|
216
296
|
fields.status = patch.status;
|
|
217
|
-
|
|
297
|
+
// R1.7: the login name is a sign-in identifier like the email: admin-only, under dominance,
|
|
298
|
+
// audited (`auth.handle.changed`, no names), sessions untouched. Validated before any write.
|
|
299
|
+
const newLoginName = requestedLoginName(patch?.loginName);
|
|
300
|
+
const oldLoginName = typeof current.fields.loginName === 'string' && current.fields.loginName !== ''
|
|
301
|
+
? (0, core_1.normalizeLoginName)(current.fields.loginName)
|
|
302
|
+
: null;
|
|
303
|
+
const loginNameChanged = newLoginName !== undefined && newLoginName !== oldLoginName;
|
|
304
|
+
if (loginNameChanged)
|
|
305
|
+
fields.loginName = newLoginName;
|
|
306
|
+
if (usernameChanged || emailChanged || statusChanged || wantsInactive || loginNameChanged) {
|
|
218
307
|
await this.ceiling.assertDominates(actor, uuid);
|
|
219
308
|
}
|
|
220
309
|
// The email first (the step most likely to be refused, 409), through the ONE helper.
|
|
221
310
|
if (emailChanged)
|
|
222
311
|
await this.changeEmail(actor, uuid, newEmail);
|
|
223
|
-
if (
|
|
312
|
+
if (loginNameChanged) {
|
|
313
|
+
// The field write and its audit row commit together (fail closed).
|
|
314
|
+
await mappingDuplicates(() => this.records.transaction(async (tx) => {
|
|
315
|
+
if (newLoginName !== null)
|
|
316
|
+
await this.assertLoginNameNotAHint(tx, newLoginName, uuid);
|
|
317
|
+
await tx.update('user', uuid, { fields });
|
|
318
|
+
await this.auditHandleChanged(tx, actor, uuid, newLoginName === null);
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
else if (Object.keys(fields).length > 0) {
|
|
224
322
|
await mappingDuplicates(() => this.records.update('user', uuid, { fields }));
|
|
225
323
|
}
|
|
226
324
|
if (statusChanged || wantsInactive)
|
|
@@ -228,6 +326,22 @@ let UserService = UserService_1 = class UserService {
|
|
|
228
326
|
if (emailChanged && oldEmail)
|
|
229
327
|
await this.notifyEmailChanged(actor, oldEmail);
|
|
230
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Review round 1, item F: a login name must not equal another principal's legacy sign-in hint
|
|
331
|
+
* (`identity_credential.ic_identity_hint`, compared under the column's case-insensitive
|
|
332
|
+
* collation); the sign-in lookup would then match two accounts. Same fixed 409 as a login-name
|
|
333
|
+
* clash (no new oracle). Read on the write's transaction through guard's credential provisioner;
|
|
334
|
+
* a hand-built service without one (or a custom provisioner without `hintHolderOf`) skips it.
|
|
335
|
+
*/
|
|
336
|
+
async assertLoginNameNotAHint(tx, loginName, principalId) {
|
|
337
|
+
const provisioner = this.provisioner;
|
|
338
|
+
if (provisioner == null || typeof provisioner.hintHolderOf !== 'function')
|
|
339
|
+
return;
|
|
340
|
+
const holder = await provisioner.hintHolderOf(loginName, (0, internal_1.transactionQueryOf)(tx));
|
|
341
|
+
if (holder !== null && !(0, http_js_1.sameUuid)(holder, principalId)) {
|
|
342
|
+
throw new identity_errors_js_1.IdentityConflictError(LOGIN_NAME_TAKEN);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
231
345
|
/**
|
|
232
346
|
* Phase 12 (KI #14): write a dominated user's new (canonical) email through guard's
|
|
233
347
|
* email-change helper — ONE transaction that locks the user and credential rows, refuses a
|
|
@@ -260,6 +374,23 @@ let UserService = UserService_1 = class UserService {
|
|
|
260
374
|
if (outcome.status !== 'changed')
|
|
261
375
|
throw new identity_errors_js_1.IdentityNotFoundError('User not found.');
|
|
262
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* R1.7 / R12.2: the `auth.handle.changed` row (type `authentication`) on the write's own
|
|
379
|
+
* transaction. Actor and target only: no login name (old or new) is written.
|
|
380
|
+
*/
|
|
381
|
+
async auditHandleChanged(tx, actor, targetId, cleared) {
|
|
382
|
+
const query = (0, internal_1.transactionQueryOf)(tx);
|
|
383
|
+
const event = {
|
|
384
|
+
type: 'authentication',
|
|
385
|
+
occurredAtMs: Date.now(),
|
|
386
|
+
actorId: actor.id,
|
|
387
|
+
tenantId: actor.tenantId,
|
|
388
|
+
action: { key: HANDLE_CHANGED },
|
|
389
|
+
resource: { level: 'record', typeKey: 'user', tenantId: actor.tenantId, recordId: targetId },
|
|
390
|
+
attributes: { changed: true, cleared },
|
|
391
|
+
};
|
|
392
|
+
await this.emailAudit.record(event, (sql, params) => query(sql, params));
|
|
393
|
+
}
|
|
263
394
|
/** The helper call with the audit row on its transaction (see {@link changeEmail}). */
|
|
264
395
|
applyEmailChange(changer, actor, uuid, newEmail) {
|
|
265
396
|
return changer.change({
|
|
@@ -365,6 +496,8 @@ exports.UserService = UserService = UserService_1 = __decorate([
|
|
|
365
496
|
__param(8, (0, common_1.Inject)(credential_provisioner_token_js_1.IDENTITY_CREDENTIAL_PROVISIONER)),
|
|
366
497
|
__param(9, (0, common_1.Optional)()),
|
|
367
498
|
__param(9, (0, common_1.Inject)(guard_1.SHIELD_MESSAGE_DISPATCHER)),
|
|
499
|
+
__param(10, (0, common_1.Optional)()),
|
|
500
|
+
__param(10, (0, common_1.Inject)(signin_admin_tokens_js_1.IDENTITY_MFA_STATUS_READER)),
|
|
368
501
|
__metadata("design:paramtypes", [engine_1.RecordsService,
|
|
369
|
-
grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object, Object, Object, Object, Object])
|
|
502
|
+
grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object, Object, Object, Object, Object, Object])
|
|
370
503
|
], UserService);
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { CompanyDepartment } from './company-department.object.js';
|
|
|
15
15
|
export { CompanyBranch } from './company-branch.object.js';
|
|
16
16
|
export { ServiceAccount } from './service-account.object.js';
|
|
17
17
|
export { StoredFile } from './stored-file.object.js';
|
|
18
|
+
export { SecurityPolicy } from './security-policy.object.js';
|
|
18
19
|
export { identityMigrationsDir } from './migrations-dir.js';
|
|
19
20
|
/**
|
|
20
21
|
* All identity objects, registered with the engine at boot (same set + order as before).
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACxF;0DAC0D;AAC1D,OAAO,EAAE,qBAAqB,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEpF;;;;;GAKG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;GAGG;AACH,YAAY,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAEhF;;;;GAIG;AACH,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,GAC5B,MAAM,+CAA+C,CAAC;AAEvD;;;GAGG;AACH,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,UAAU,GAChB,MAAM,mCAAmC,CAAC;AAU3C;;;GAGG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAEpF;;;;;;;GAOG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACxF;0DAC0D;AAC1D,OAAO,EAAE,qBAAqB,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEpF;;;;;GAKG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;GAGG;AACH,YAAY,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAEhF;;;;GAIG;AACH,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,GAC5B,MAAM,+CAA+C,CAAC;AAEvD;;;GAGG;AACH,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,UAAU,GAChB,MAAM,mCAAmC,CAAC;AAU3C;;;GAGG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAEpF;;;;;;;GAOG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DbMembershipChecker = exports.DbScopeSubtreeResolver = exports.AlsSessionFactsReader = exports.FilesService = exports.OrgBootstrapError = exports.runOrgBootstrap = exports.IdentityFeatureModule = exports.buildIdentityAuthKeys = exports.identityControlPlaneKeys = exports.identityAuthActionKeys = exports.identityObjects = exports.identityMigrationsDir = exports.StoredFile = exports.ServiceAccount = exports.CompanyBranch = exports.CompanyDepartment = exports.MembershipPosting = exports.CompanyMembership = exports.Branch = exports.Department = exports.Role = exports.User = exports.Company = void 0;
|
|
3
|
+
exports.DbMembershipChecker = exports.DbScopeSubtreeResolver = exports.AlsSessionFactsReader = exports.FilesService = exports.OrgBootstrapError = exports.runOrgBootstrap = exports.IdentityFeatureModule = exports.buildIdentityAuthKeys = exports.identityControlPlaneKeys = exports.identityAuthActionKeys = exports.identityObjects = exports.identityMigrationsDir = exports.SecurityPolicy = exports.StoredFile = exports.ServiceAccount = exports.CompanyBranch = exports.CompanyDepartment = exports.MembershipPosting = exports.CompanyMembership = exports.Branch = exports.Department = exports.Role = exports.User = exports.Company = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* @astram/identity — the framework-owned identity + tenancy model.
|
|
6
6
|
* The 9 identity object classes (relocated from the consumer app in Sub-plan B) +
|
|
@@ -29,6 +29,8 @@ var service_account_object_js_1 = require("./service-account.object.js");
|
|
|
29
29
|
Object.defineProperty(exports, "ServiceAccount", { enumerable: true, get: function () { return service_account_object_js_1.ServiceAccount; } });
|
|
30
30
|
var stored_file_object_js_1 = require("./stored-file.object.js");
|
|
31
31
|
Object.defineProperty(exports, "StoredFile", { enumerable: true, get: function () { return stored_file_object_js_1.StoredFile; } });
|
|
32
|
+
var security_policy_object_js_1 = require("./security-policy.object.js");
|
|
33
|
+
Object.defineProperty(exports, "SecurityPolicy", { enumerable: true, get: function () { return security_policy_object_js_1.SecurityPolicy; } });
|
|
32
34
|
var migrations_dir_js_1 = require("./migrations-dir.js");
|
|
33
35
|
Object.defineProperty(exports, "identityMigrationsDir", { enumerable: true, get: function () { return migrations_dir_js_1.identityMigrationsDir; } });
|
|
34
36
|
/**
|
package/dist/objects.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"objects.d.ts","sourceRoot":"","sources":["../src/objects.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"objects.d.ts","sourceRoot":"","sources":["../src/objects.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAiBtD,6FAA6F;AAC7F,eAAO,MAAM,eAAe,EAAE,SAAS,eAAe,EAgBrD,CAAC"}
|
package/dist/objects.js
CHANGED
|
@@ -15,6 +15,7 @@ const record_share_object_js_1 = require("./record-share.object.js");
|
|
|
15
15
|
const shared_resource_object_js_1 = require("./shared-resource.object.js");
|
|
16
16
|
const service_account_object_js_1 = require("./service-account.object.js");
|
|
17
17
|
const stored_file_object_js_1 = require("./stored-file.object.js");
|
|
18
|
+
const security_policy_object_js_1 = require("./security-policy.object.js");
|
|
18
19
|
/** All identity objects, registered with the engine at boot (same set + order as before). */
|
|
19
20
|
exports.identityObjects = [
|
|
20
21
|
company_object_js_1.Company,
|
|
@@ -31,4 +32,5 @@ exports.identityObjects = [
|
|
|
31
32
|
shared_resource_object_js_1.SharedResource,
|
|
32
33
|
service_account_object_js_1.ServiceAccount,
|
|
33
34
|
stored_file_object_js_1.StoredFile,
|
|
35
|
+
security_policy_object_js_1.SecurityPolicy,
|
|
34
36
|
];
|
package/dist/role.object.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.object.d.ts","sourceRoot":"","sources":["../src/role.object.ts"],"names":[],"mappings":"AAGA,
|
|
1
|
+
{"version":3,"file":"role.object.d.ts","sourceRoot":"","sources":["../src/role.object.ts"],"names":[],"mappings":"AAGA,uFAAuF;AACvF,qBAWa,IAAI;IAEf,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAMd,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAGjC,SAAS,EAAG,MAAM,CAAC;CACpB"}
|