@astram/identity 0.12.0 → 0.14.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/features/authz/grant-admin.service.d.ts +1 -1
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +8 -0
- package/dist/features/authz/grant-ceiling.d.ts +11 -3
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +20 -3
- package/dist/features/authz/record-authorization.d.ts +9 -1
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/record-authorization.js +24 -3
- package/dist/features/company/company-hierarchy.service.d.ts +6 -0
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
- package/dist/features/company/company-hierarchy.service.js +19 -0
- package/dist/features/identity-feature.module.d.ts +7 -1
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +25 -5
- package/dist/features/membership/membership.service.d.ts +9 -0
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +40 -0
- package/dist/features/membership/posting.service.d.ts +18 -0
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +85 -27
- package/dist/features/org-bootstrap/org-bootstrap-error.d.ts +20 -0
- package/dist/features/org-bootstrap/org-bootstrap-error.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-error.js +24 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +58 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.js +323 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +79 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.js +288 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.harness.d.ts +259 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.harness.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.harness.js +604 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.mocks.d.ts +24 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.mocks.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.mocks.js +39 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts +19 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.js +30 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts +8 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.js +10 -0
- package/dist/features/org-bootstrap/org-names.d.ts +12 -0
- package/dist/features/org-bootstrap/org-names.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-names.js +26 -0
- package/dist/features/org-bootstrap/org-state.reader.d.ts +15 -0
- package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-state.reader.js +225 -0
- package/dist/features/org-bootstrap/root-password-source.d.ts +9 -0
- package/dist/features/org-bootstrap/root-password-source.d.ts.map +1 -0
- package/dist/features/org-bootstrap/root-password-source.js +57 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +42 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.js +688 -0
- package/dist/features/org-bootstrap/set-password-links.d.ts +30 -0
- package/dist/features/org-bootstrap/set-password-links.d.ts.map +1 -0
- package/dist/features/org-bootstrap/set-password-links.js +55 -0
- package/dist/features/org-structure/org-catalog.service.d.ts +13 -0
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.service.js +40 -0
- package/dist/features/role/role-owner-transition.d.ts +17 -0
- package/dist/features/role/role-owner-transition.d.ts.map +1 -0
- package/dist/features/role/role-owner-transition.js +23 -0
- package/dist/features/role/role.controller.d.ts +1 -1
- package/dist/features/role/role.controller.d.ts.map +1 -1
- package/dist/features/role/role.controller.js +6 -4
- package/dist/features/role/role.service.d.ts +8 -2
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +13 -3
- package/dist/features/user/credential-provisioner.token.d.ts +8 -0
- package/dist/features/user/credential-provisioner.token.d.ts.map +1 -0
- package/dist/features/user/credential-provisioner.token.js +10 -0
- package/dist/features/user/user.controller.d.ts +1 -3
- package/dist/features/user/user.controller.d.ts.map +1 -1
- package/dist/features/user/user.controller.js +2 -2
- package/dist/features/user/user.service.d.ts +28 -5
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +45 -12
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -1
- package/dist/role.object.d.ts +1 -0
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +8 -0
- package/migrations/1710000000101-AddRoleOwnerCompany.cjs +98 -0
- package/package.json +4 -4
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ResetTokenStore } from '@astram/guard';
|
|
2
|
+
export declare const DEFAULT_LINK_TTL_HOURS = 72;
|
|
3
|
+
/** Whole hours, 1 to 168. */
|
|
4
|
+
export declare function validateLinkTtlHours(hours: number): number;
|
|
5
|
+
/**
|
|
6
|
+
* The reset-page base, under forgot-password's rule: outside development/test it must be set, http(s)
|
|
7
|
+
* and not loopback. Guard's resolver also checks the email-change base, which this run never uses, so
|
|
8
|
+
* that one is given a placeholder that always passes. The configured value is never echoed.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveSetPasswordBase(env: NodeJS.ProcessEnv): string;
|
|
11
|
+
export interface LinkTarget {
|
|
12
|
+
username: string;
|
|
13
|
+
principalId: string;
|
|
14
|
+
/** The user's company (audit tenant). */
|
|
15
|
+
tenantId: string;
|
|
16
|
+
/** Existing user whose earlier unused tokens must die first. */
|
|
17
|
+
invalidateFirst: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface IssueLinksInput {
|
|
20
|
+
store: ResetTokenStore;
|
|
21
|
+
targets: readonly LinkTarget[];
|
|
22
|
+
base: string;
|
|
23
|
+
ttlHours: number;
|
|
24
|
+
out: (line: string) => void;
|
|
25
|
+
/** Writes the `credential.setup_link_issued` audit row (user and expiry only). */
|
|
26
|
+
audit: (target: LinkTarget, expiresAtMs: number) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/** Issue one token per target and print its link once. Returns how many were issued. */
|
|
29
|
+
export declare function issueSetPasswordLinks(input: IssueLinksInput): Promise<number>;
|
|
30
|
+
//# sourceMappingURL=set-password-links.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set-password-links.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/set-password-links.ts"],"names":[],"mappings":"AAMA,OAAO,EAA2B,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAG9E,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAGzC,6BAA6B;AAC7B,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ1D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAarE;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,kFAAkF;IAClF,KAAK,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE;AAED,wFAAwF;AACxF,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBnF"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_LINK_TTL_HOURS = void 0;
|
|
4
|
+
exports.validateLinkTtlHours = validateLinkTtlHours;
|
|
5
|
+
exports.resolveSetPasswordBase = resolveSetPasswordBase;
|
|
6
|
+
exports.issueSetPasswordLinks = issueSetPasswordLinks;
|
|
7
|
+
/**
|
|
8
|
+
* One-time set-password links (G-009 R8): the same token mechanism as forgot-password (32 random bytes,
|
|
9
|
+
* stored only as sha256 in the reset token store, per-row expiry), printed once to the operator's
|
|
10
|
+
* terminal. A token or link never reaches the audit row, the report or an error.
|
|
11
|
+
*/
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const guard_1 = require("@astram/guard");
|
|
14
|
+
const org_bootstrap_error_js_1 = require("./org-bootstrap-error.js");
|
|
15
|
+
exports.DEFAULT_LINK_TTL_HOURS = 72;
|
|
16
|
+
const HOUR_MS = 3_600_000;
|
|
17
|
+
/** Whole hours, 1 to 168. */
|
|
18
|
+
function validateLinkTtlHours(hours) {
|
|
19
|
+
if (!Number.isInteger(hours) || hours < 1 || hours > 168) {
|
|
20
|
+
throw new org_bootstrap_error_js_1.OrgBootstrapError('invalid_link_ttl', 'The link lifetime must be 1 to 168 whole hours.');
|
|
21
|
+
}
|
|
22
|
+
return hours;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The reset-page base, under forgot-password's rule: outside development/test it must be set, http(s)
|
|
26
|
+
* and not loopback. Guard's resolver also checks the email-change base, which this run never uses, so
|
|
27
|
+
* that one is given a placeholder that always passes. The configured value is never echoed.
|
|
28
|
+
*/
|
|
29
|
+
function resolveSetPasswordBase(env) {
|
|
30
|
+
try {
|
|
31
|
+
return (0, guard_1.resolveAuthLinkBaseUrls)({
|
|
32
|
+
env,
|
|
33
|
+
emailChangeLinkBaseUrlOption: 'https://email-change.not-used.example/',
|
|
34
|
+
mailDisabled: false,
|
|
35
|
+
}).resetLinkBaseUrl;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new org_bootstrap_error_js_1.OrgBootstrapError('link_base_url', 'RESET_LINK_BASE_URL must be set to a public http(s) URL (not localhost) outside development/test.');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Issue one token per target and print its link once. Returns how many were issued. */
|
|
42
|
+
async function issueSetPasswordLinks(input) {
|
|
43
|
+
let issued = 0;
|
|
44
|
+
for (const target of input.targets) {
|
|
45
|
+
if (target.invalidateFirst)
|
|
46
|
+
await input.store.invalidateAllForPrincipal(target.principalId);
|
|
47
|
+
const raw = (0, node_crypto_1.randomBytes)(32).toString('hex');
|
|
48
|
+
const expiresAtMs = Date.now() + input.ttlHours * HOUR_MS;
|
|
49
|
+
await input.store.issue(target.principalId, (0, node_crypto_1.createHash)('sha256').update(raw).digest('hex'), expiresAtMs);
|
|
50
|
+
await input.audit(target, expiresAtMs);
|
|
51
|
+
input.out(`${target.username} ${input.base}?token=${raw} (expires ${new Date(expiresAtMs).toISOString()})`);
|
|
52
|
+
issued += 1;
|
|
53
|
+
}
|
|
54
|
+
return issued;
|
|
55
|
+
}
|
|
@@ -13,6 +13,19 @@ export declare class OrgCatalogService {
|
|
|
13
13
|
listCompanyDepartments(companyUuid: string): Promise<NamedOption[]>;
|
|
14
14
|
/** A company's branches as `{ uuid, name }` options for the posting editor. */
|
|
15
15
|
listCompanyBranches(companyUuid: string): Promise<NamedOption[]>;
|
|
16
|
+
/**
|
|
17
|
+
* The id of the catalog row named `name`, creating it when none exists (G-009 R5). The catalog
|
|
18
|
+
* is global, so the lookup is by name; the create runs through the engine pipe like any write.
|
|
19
|
+
*/
|
|
20
|
+
ensureDepartment(name: string): Promise<string>;
|
|
21
|
+
/** The id of the branch named `name`, creating it when none exists. */
|
|
22
|
+
ensureBranch(name: string): Promise<string>;
|
|
23
|
+
/** Link a department to a company (the junction row); idempotent. */
|
|
24
|
+
attachDepartment(companyUuid: string, departmentUuid: string): Promise<void>;
|
|
25
|
+
/** Link a branch to a company (the junction row); idempotent. */
|
|
26
|
+
attachBranch(companyUuid: string, branchUuid: string): Promise<void>;
|
|
27
|
+
private ensureCatalogRow;
|
|
28
|
+
private attachJunction;
|
|
16
29
|
/** Shared reader for a company-scoped catalog junction; lists then name-resolves. */
|
|
17
30
|
private listCompanyCatalog;
|
|
18
31
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org-catalog.service.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"org-catalog.service.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,cAAc,EAAiB,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAQ7D,qBACa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,cAAc;IAEpD,kFAAkF;IAClF,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IASnE,+EAA+E;IAC/E,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAIhE;;;OAGG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI/C,uEAAuE;IACvE,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI3C,qEAAqE;IACrE,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5E,iEAAiE;IACjE,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAItD,gBAAgB;YAOhB,cAAc;IAa5B,qFAAqF;YACvE,kBAAkB;CAoBjC"}
|
|
@@ -18,6 +18,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
18
18
|
exports.OrgCatalogService = void 0;
|
|
19
19
|
const common_1 = require("@nestjs/common");
|
|
20
20
|
const engine_1 = require("@astram/engine");
|
|
21
|
+
/** Department/branch rows have a single uuid key; a composite id here is a wiring defect. */
|
|
22
|
+
function catalogId(id) {
|
|
23
|
+
if (typeof id !== 'string')
|
|
24
|
+
throw new TypeError('catalog row id is not a single uuid');
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
21
27
|
let OrgCatalogService = class OrgCatalogService {
|
|
22
28
|
records;
|
|
23
29
|
constructor(records) {
|
|
@@ -31,6 +37,40 @@ let OrgCatalogService = class OrgCatalogService {
|
|
|
31
37
|
listCompanyBranches(companyUuid) {
|
|
32
38
|
return this.listCompanyCatalog('company_branch', 'branchUuid', 'branch', companyUuid);
|
|
33
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The id of the catalog row named `name`, creating it when none exists (G-009 R5). The catalog
|
|
42
|
+
* is global, so the lookup is by name; the create runs through the engine pipe like any write.
|
|
43
|
+
*/
|
|
44
|
+
ensureDepartment(name) {
|
|
45
|
+
return this.ensureCatalogRow('department', name);
|
|
46
|
+
}
|
|
47
|
+
/** The id of the branch named `name`, creating it when none exists. */
|
|
48
|
+
ensureBranch(name) {
|
|
49
|
+
return this.ensureCatalogRow('branch', name);
|
|
50
|
+
}
|
|
51
|
+
/** Link a department to a company (the junction row); idempotent. */
|
|
52
|
+
attachDepartment(companyUuid, departmentUuid) {
|
|
53
|
+
return this.attachJunction('company_department', 'departmentUuid', companyUuid, departmentUuid);
|
|
54
|
+
}
|
|
55
|
+
/** Link a branch to a company (the junction row); idempotent. */
|
|
56
|
+
attachBranch(companyUuid, branchUuid) {
|
|
57
|
+
return this.attachJunction('company_branch', 'branchUuid', companyUuid, branchUuid);
|
|
58
|
+
}
|
|
59
|
+
async ensureCatalogRow(typeKey, name) {
|
|
60
|
+
const page = await this.records.list(typeKey, { where: { name } });
|
|
61
|
+
const existing = page.rows[0];
|
|
62
|
+
if (existing !== undefined)
|
|
63
|
+
return catalogId(existing.id);
|
|
64
|
+
return catalogId(await this.records.create(typeKey, { fields: { name } }));
|
|
65
|
+
}
|
|
66
|
+
async attachJunction(junctionTypeKey, refFieldKey, companyUuid, refUuid) {
|
|
67
|
+
const page = await this.records.list(junctionTypeKey, { where: { companyUuid } });
|
|
68
|
+
if (page.rows.some((r) => r.fields[refFieldKey] === refUuid))
|
|
69
|
+
return;
|
|
70
|
+
await this.records.create(junctionTypeKey, {
|
|
71
|
+
fields: { companyUuid, [refFieldKey]: refUuid },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
34
74
|
/** Shared reader for a company-scoped catalog junction; lists then name-resolves. */
|
|
35
75
|
async listCompanyCatalog(junctionTypeKey, refFieldKey, catalogTypeKey, companyUuid) {
|
|
36
76
|
const page = await this.records.list(junctionTypeKey, { where: { companyUuid } });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONLY writer of `roles.r_owner_company_uuid` after create (G-009 T14, SPEC R13 v5). The column
|
|
3
|
+
* is immutable at the engine, so an owner change is this audited compare-and-set on the caller's
|
|
4
|
+
* connection (inside its transaction): company -> NULL (company delete) or NULL -> company (owner
|
|
5
|
+
* claim). Any other shape (company -> company, NULL -> NULL, same -> same) is refused with no write.
|
|
6
|
+
* Exactly one row must change; a stale `from` or a failing audit throws, rolling the caller back.
|
|
7
|
+
*/
|
|
8
|
+
import { type IdentitySchemaNames } from '../../schema.js';
|
|
9
|
+
export type RoleOwnerQuery = (sql: string, params?: readonly unknown[]) => Promise<unknown>;
|
|
10
|
+
/** Writes the audit row for the transition on the SAME query (connection) as the update. */
|
|
11
|
+
export type RoleOwnerAudit = (action: 'role.owner_cleared' | 'role.owner_claimed', attributes: {
|
|
12
|
+
roleUuid: string;
|
|
13
|
+
from: string | null;
|
|
14
|
+
to: string | null;
|
|
15
|
+
}, query: RoleOwnerQuery) => Promise<void>;
|
|
16
|
+
export declare function transitionRoleOwner(query: RoleOwnerQuery, roleUuid: string, from: string | null, to: string | null, audit: RoleOwnerAudit, schemas?: IdentitySchemaNames): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=role-owner-transition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-owner-transition.d.ts","sourceRoot":"","sources":["../../../src/features/role/role-owner-transition.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEpF,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAE5F,4FAA4F;AAC5F,MAAM,MAAM,cAAc,GAAG,CAC3B,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,EACnD,UAAU,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACxE,KAAK,EAAE,cAAc,KAClB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,cAAc,EACrB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,EAAE,EAAE,MAAM,GAAG,IAAI,EACjB,KAAK,EAAE,cAAc,EACrB,OAAO,GAAE,mBAA+C,GACvD,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.transitionRoleOwner = transitionRoleOwner;
|
|
4
|
+
/**
|
|
5
|
+
* The ONLY writer of `roles.r_owner_company_uuid` after create (G-009 T14, SPEC R13 v5). The column
|
|
6
|
+
* is immutable at the engine, so an owner change is this audited compare-and-set on the caller's
|
|
7
|
+
* connection (inside its transaction): company -> NULL (company delete) or NULL -> company (owner
|
|
8
|
+
* claim). Any other shape (company -> company, NULL -> NULL, same -> same) is refused with no write.
|
|
9
|
+
* Exactly one row must change; a stale `from` or a failing audit throws, rolling the caller back.
|
|
10
|
+
*/
|
|
11
|
+
const schema_js_1 = require("../../schema.js");
|
|
12
|
+
async function transitionRoleOwner(query, roleUuid, from, to, audit, schemas = (0, schema_js_1.frameworkSchemasFromEnv)()) {
|
|
13
|
+
const clearing = from !== null && to === null;
|
|
14
|
+
const claiming = from === null && to !== null;
|
|
15
|
+
if (!clearing && !claiming) {
|
|
16
|
+
throw new Error('A role owner may only change from a company to none, or from none to a company.');
|
|
17
|
+
}
|
|
18
|
+
const res = (await query(`UPDATE \`${schemas.app}\`.\`roles\` SET r_owner_company_uuid = ? WHERE r_uuid = ? AND r_owner_company_uuid <=> ?`, [to, roleUuid, from]));
|
|
19
|
+
if (Number(res?.affectedRows ?? 0) !== 1) {
|
|
20
|
+
throw new Error('The role owner changed concurrently; the transition was not applied.');
|
|
21
|
+
}
|
|
22
|
+
await audit(clearing ? 'role.owner_cleared' : 'role.owner_claimed', { roleUuid, from, to }, query);
|
|
23
|
+
}
|
|
@@ -16,7 +16,7 @@ export declare class RoleController {
|
|
|
16
16
|
limit: number;
|
|
17
17
|
}>;
|
|
18
18
|
/** Create a role — gated on `identity:role:create`. */
|
|
19
|
-
createRole(body: {
|
|
19
|
+
createRole(req: AuthedRequest, body: {
|
|
20
20
|
name?: string;
|
|
21
21
|
}): Promise<{
|
|
22
22
|
id: import("packages/engine/dist/types.js").RecordId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.controller.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.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,uDAAuD;IAGjD,UAAU,
|
|
1
|
+
{"version":3,"file":"role.controller.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.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,uDAAuD;IAGjD,UAAU,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAM3E;0CACsC;IAGtC,OAAO,CAA0B,IAAI,EAAE,MAAM;IAI7C,uDAAuD;IAGjD,UAAU,CACP,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMjC,uDAAuD;IAGjD,UAAU,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIlF"}
|
|
@@ -38,8 +38,9 @@ let RoleController = class RoleController {
|
|
|
38
38
|
return { total: result.total, rows: result.rows, page, limit };
|
|
39
39
|
}
|
|
40
40
|
/** Create a role — gated on `identity:role:create`. */
|
|
41
|
-
async createRole(body) {
|
|
42
|
-
|
|
41
|
+
async createRole(req, body) {
|
|
42
|
+
// The owner is derived from the principal in the service; any owner in the body is ignored.
|
|
43
|
+
const id = await this.roles.createRole(body?.name ?? 'Untitled', req.principal);
|
|
43
44
|
return { id };
|
|
44
45
|
}
|
|
45
46
|
/** Read one role — gated on `identity:role:read` (CONTROL-plane; root, the identity
|
|
@@ -70,9 +71,10 @@ __decorate([
|
|
|
70
71
|
__decorate([
|
|
71
72
|
(0, common_1.Post)('roles'),
|
|
72
73
|
(0, guard_1.Classified)({ typeKey: 'role', action: 'create' }),
|
|
73
|
-
__param(0, (0, common_1.
|
|
74
|
+
__param(0, (0, common_1.Req)()),
|
|
75
|
+
__param(1, (0, common_1.Body)()),
|
|
74
76
|
__metadata("design:type", Function),
|
|
75
|
-
__metadata("design:paramtypes", [Object]),
|
|
77
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
76
78
|
__metadata("design:returntype", Promise)
|
|
77
79
|
], RoleController.prototype, "createRole", null);
|
|
78
80
|
__decorate([
|
|
@@ -20,8 +20,14 @@ export declare class RoleService {
|
|
|
20
20
|
private readonly versionBump?;
|
|
21
21
|
private readonly grants?;
|
|
22
22
|
constructor(records: RecordsService, ceiling: GrantCeiling, versionBump?: (PostingVersionBump | null) | undefined, grants?: (GrantAdminService | null) | undefined);
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Create a role. The owner is derived here from the actor, never from a client value: a non-root
|
|
25
|
+
* actor's role is owned by its active company (else its tenant); root's role is shared (no owner).
|
|
26
|
+
* A root-flagged actor bound to a declared operation (the org bootstrap's `roles` step, Shield #9)
|
|
27
|
+
* is anchored at the tenant it creates roles for, so its role is owned by that tenant.
|
|
28
|
+
* No actor (internal callers) ⇒ no owner field.
|
|
29
|
+
*/
|
|
30
|
+
createRole(name: string, actor?: Principal): Promise<RecordId>;
|
|
25
31
|
/** List roles (tenant-scoped by the pipe), with paging/search/sort. */
|
|
26
32
|
listRoles(params?: ListParams): Promise<import("@astram/engine").RecordPage>;
|
|
27
33
|
/** Read a single role. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.service.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"role.service.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAe,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uCAAuC,CAAC;AAK/C;;;;;;;;;GASG;AACH,qBACa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAKxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAK7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAXP,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EAKrB,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA,EAKvC,MAAM,CAAC,GAAE,iBAAiB,GAAG,IAAI,aAAA;IAGpD;;;;;;OAMG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQ9D,uEAAuE;IACvE,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU;IAI7B,0BAA0B;IAC1B,OAAO,CAAC,IAAI,EAAE,MAAM;IAIpB,8FAA8F;IACxF,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO1F;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACG,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAoBhE"}
|
|
@@ -43,9 +43,19 @@ let RoleService = class RoleService {
|
|
|
43
43
|
this.versionBump = versionBump;
|
|
44
44
|
this.grants = grants;
|
|
45
45
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Create a role. The owner is derived here from the actor, never from a client value: a non-root
|
|
48
|
+
* actor's role is owned by its active company (else its tenant); root's role is shared (no owner).
|
|
49
|
+
* A root-flagged actor bound to a declared operation (the org bootstrap's `roles` step, Shield #9)
|
|
50
|
+
* is anchored at the tenant it creates roles for, so its role is owned by that tenant.
|
|
51
|
+
* No actor (internal callers) ⇒ no owner field.
|
|
52
|
+
*/
|
|
53
|
+
createRole(name, actor) {
|
|
54
|
+
const fields = { name };
|
|
55
|
+
if (actor && (actor.isRoot !== true || (0, engine_1.operationOf)(actor) !== undefined)) {
|
|
56
|
+
fields.ownerCompanyUuid = actor.activeScopeNodeId ?? actor.tenantId;
|
|
57
|
+
}
|
|
58
|
+
return this.records.create('role', { fields });
|
|
49
59
|
}
|
|
50
60
|
/** List roles (tenant-scoped by the pipe), with paging/search/sort. */
|
|
51
61
|
listRoles(params) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DI token for the credential provisioner `UserService.createUserWithCredential` uses (G-009 T7,
|
|
3
|
+
* OD-6): guard's `MariaDbCredentialProvisioner` (`@astram/guard/internal`), built by `IdentityFeatureModule.forRoot()` over
|
|
4
|
+
* the ONE shared authz pool. It writes the locked credential on the engine transaction's own
|
|
5
|
+
* connection, so an API-created user and its credential commit or fail together.
|
|
6
|
+
*/
|
|
7
|
+
export declare const IDENTITY_CREDENTIAL_PROVISIONER: unique symbol;
|
|
8
|
+
//# sourceMappingURL=credential-provisioner.token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-provisioner.token.d.ts","sourceRoot":"","sources":["../../../src/features/user/credential-provisioner.token.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,eAA4C,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IDENTITY_CREDENTIAL_PROVISIONER = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* DI token for the credential provisioner `UserService.createUserWithCredential` uses (G-009 T7,
|
|
6
|
+
* OD-6): guard's `MariaDbCredentialProvisioner` (`@astram/guard/internal`), built by `IdentityFeatureModule.forRoot()` over
|
|
7
|
+
* the ONE shared authz pool. It writes the locked credential on the engine transaction's own
|
|
8
|
+
* connection, so an API-created user and its credential commit or fail together.
|
|
9
|
+
*/
|
|
10
|
+
exports.IDENTITY_CREDENTIAL_PROVISIONER = Symbol('IDENTITY_CREDENTIAL_PROVISIONER');
|
|
@@ -23,9 +23,7 @@ export declare class UserController {
|
|
|
23
23
|
username?: string;
|
|
24
24
|
email?: string;
|
|
25
25
|
companyUuid?: string;
|
|
26
|
-
}): Promise<
|
|
27
|
-
id: import("packages/engine/dist/types.js").RecordId;
|
|
28
|
-
}>;
|
|
26
|
+
}): Promise<import("./user.service.js").CreateUserResult>;
|
|
29
27
|
/** Read one user — gated on `identity:user:read` (CONTROL-plane; root, the identity
|
|
30
28
|
* administrator, may read users). */
|
|
31
29
|
getUser(uuid: string): Promise<import("packages/engine/dist/types.js").RecordView | null>;
|
|
@@ -1 +1 @@
|
|
|
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,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE
|
|
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,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;IAY3E;0CACsC;IAGtC,OAAO,CAA0B,IAAI,EAAE,MAAM;IAI7C,iGAAiG;IAG3F,UAAU,CACP,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,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,EAA2B,IAAI,EAAE,MAAM;;;CAIlF"}
|
|
@@ -43,13 +43,13 @@ let UserController = class UserController {
|
|
|
43
43
|
*/
|
|
44
44
|
async createUser(req, body) {
|
|
45
45
|
const companyUuid = (0, guard_1.optionalUuidField)(body?.companyUuid, 'companyUuid');
|
|
46
|
-
const
|
|
46
|
+
const result = await this.users.createUserWithCredential(req.principal, {
|
|
47
47
|
username: body?.username ?? 'Untitled',
|
|
48
48
|
// KI #94: the template form sends it; the service canonicalises and validates it.
|
|
49
49
|
...(typeof body?.email === 'string' ? { email: body.email } : {}),
|
|
50
50
|
...(companyUuid !== undefined ? { companyUuid } : {}),
|
|
51
51
|
});
|
|
52
|
-
return
|
|
52
|
+
return result;
|
|
53
53
|
}
|
|
54
54
|
/** Read one user — gated on `identity:user:read` (CONTROL-plane; root, the identity
|
|
55
55
|
* administrator, may read users). */
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { RecordsService, type RecordId } from '@astram/engine';
|
|
2
2
|
import type { FrameworkSchemas, Principal } from '@astram/core';
|
|
3
|
-
import { type EmailAddressChanger, type EmailSender, type ResetTokenStore, type SessionSweeper } from '@astram/guard';
|
|
3
|
+
import { type EmailAddressChanger, type CredentialProvisioner, type EmailSender, 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
|
+
/** The outcome of {@link UserService.createUserWithCredential}. */
|
|
8
|
+
export type CreateUserResult = {
|
|
9
|
+
id: RecordId;
|
|
10
|
+
credential: 'locked';
|
|
11
|
+
} | {
|
|
12
|
+
id: RecordId;
|
|
13
|
+
credential: 'none';
|
|
14
|
+
reason: 'no_email' | 'no_company' | 'no_provisioner';
|
|
15
|
+
};
|
|
7
16
|
/** The patch keys a generic user update accepts (security hardening #3). */
|
|
8
17
|
export interface UserPatch {
|
|
9
18
|
username?: unknown;
|
|
@@ -37,6 +46,7 @@ export declare class UserService {
|
|
|
37
46
|
private readonly sessions?;
|
|
38
47
|
private readonly versionBump?;
|
|
39
48
|
private readonly emailAddresses?;
|
|
49
|
+
private readonly provisioner?;
|
|
40
50
|
private readonly logger;
|
|
41
51
|
/**
|
|
42
52
|
* The engine's audit sink, used on the email helper's transaction (no pool of its own), on the
|
|
@@ -44,7 +54,16 @@ export declare class UserService {
|
|
|
44
54
|
* service) ⇒ the sink resolves `SHIELD_DB_NAMESPACE` itself.
|
|
45
55
|
*/
|
|
46
56
|
private readonly emailAudit;
|
|
47
|
-
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);
|
|
57
|
+
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);
|
|
58
|
+
/**
|
|
59
|
+
* Create a user; returns its id. Delegates to {@link createUserWithCredential} (same behaviour;
|
|
60
|
+
* the detailed credential outcome is dropped to keep this public signature stable).
|
|
61
|
+
*/
|
|
62
|
+
createUser(actor: Principal, input: {
|
|
63
|
+
username: string;
|
|
64
|
+
email?: string | undefined;
|
|
65
|
+
companyUuid?: string | undefined;
|
|
66
|
+
}): Promise<RecordId>;
|
|
48
67
|
/**
|
|
49
68
|
* Create a user. With `companyUuid`, the user is created AND attached (one active membership) in
|
|
50
69
|
* ONE transaction — the onboarding path for a non-root admin, whose membership create is checked
|
|
@@ -54,13 +73,17 @@ export declare class UserService {
|
|
|
54
73
|
*
|
|
55
74
|
* KI #94: an `email` is stored canonical (the ONE address rule, as the email-change flow); a
|
|
56
75
|
* malformed one ⇒ {@link IdentityBadRequestError} (400) before any write; a blank one is no email.
|
|
57
|
-
*
|
|
76
|
+
*
|
|
77
|
+
* G-009 T7 (OD-6): with an email AND a company, a LOCKED credential (no password verifies; set
|
|
78
|
+
* later by reset) is created in that same transaction through the provisioner on the engine
|
|
79
|
+
* transaction's own connection; a provisioner failure rolls everything back. Otherwise no
|
|
80
|
+
* credential is created and the result says why (no email takes precedence).
|
|
58
81
|
*/
|
|
59
|
-
|
|
82
|
+
createUserWithCredential(actor: Principal, input: {
|
|
60
83
|
username: string;
|
|
61
84
|
email?: string | undefined;
|
|
62
85
|
companyUuid?: string | undefined;
|
|
63
|
-
}): Promise<
|
|
86
|
+
}): Promise<CreateUserResult>;
|
|
64
87
|
/** List users (tenant-scoped by the pipe), with paging/search/sort. */
|
|
65
88
|
listUsers(params?: ListParams): Promise<import("@astram/engine").RecordPage>;
|
|
66
89
|
/** Read a single user. */
|
|
@@ -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,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,12 +16,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16
16
|
exports.UserService = void 0;
|
|
17
17
|
const common_1 = require("@nestjs/common");
|
|
18
18
|
const engine_1 = require("@astram/engine");
|
|
19
|
+
const internal_1 = require("@astram/engine/internal");
|
|
19
20
|
const guard_1 = require("@astram/guard");
|
|
20
21
|
const posting_version_bump_js_1 = require("../membership/posting-version-bump.js");
|
|
21
22
|
const list_query_js_1 = require("../shared/list-query.js");
|
|
22
23
|
const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
|
|
23
24
|
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
24
25
|
const email_address_changer_token_js_1 = require("./email-address-changer.token.js");
|
|
26
|
+
const credential_provisioner_token_js_1 = require("./credential-provisioner.token.js");
|
|
25
27
|
const framework_schemas_token_js_1 = require("../framework-schemas.token.js");
|
|
26
28
|
/**
|
|
27
29
|
* KI #87: `users` has two unique keys besides the PK, `IDX_users_username` (migration 001) and
|
|
@@ -43,8 +45,11 @@ async function mappingDuplicates(write) {
|
|
|
43
45
|
return await write();
|
|
44
46
|
}
|
|
45
47
|
catch (err) {
|
|
46
|
-
|
|
48
|
+
// R1-4: an orphan credential already holds this login hint (deleteUser leaves it): same fixed
|
|
49
|
+
// 409 as a duplicate email, never the hint.
|
|
50
|
+
if (isDuplicateKey(err) || err instanceof guard_1.CredentialExistsError) {
|
|
47
51
|
throw new identity_errors_js_1.IdentityConflictError(USERNAME_OR_EMAIL_TAKEN);
|
|
52
|
+
}
|
|
48
53
|
throw err;
|
|
49
54
|
}
|
|
50
55
|
}
|
|
@@ -77,6 +82,7 @@ let UserService = UserService_1 = class UserService {
|
|
|
77
82
|
sessions;
|
|
78
83
|
versionBump;
|
|
79
84
|
emailAddresses;
|
|
85
|
+
provisioner;
|
|
80
86
|
logger = new common_1.Logger(UserService_1.name);
|
|
81
87
|
/**
|
|
82
88
|
* The engine's audit sink, used on the email helper's transaction (no pool of its own), on the
|
|
@@ -84,7 +90,7 @@ let UserService = UserService_1 = class UserService {
|
|
|
84
90
|
* service) ⇒ the sink resolves `SHIELD_DB_NAMESPACE` itself.
|
|
85
91
|
*/
|
|
86
92
|
emailAudit;
|
|
87
|
-
constructor(records, ceiling, resetTokens, emailSender, sessions, versionBump, emailAddresses, schemas) {
|
|
93
|
+
constructor(records, ceiling, resetTokens, emailSender, sessions, versionBump, emailAddresses, schemas, provisioner) {
|
|
88
94
|
this.records = records;
|
|
89
95
|
this.ceiling = ceiling;
|
|
90
96
|
this.resetTokens = resetTokens;
|
|
@@ -92,8 +98,16 @@ let UserService = UserService_1 = class UserService {
|
|
|
92
98
|
this.sessions = sessions;
|
|
93
99
|
this.versionBump = versionBump;
|
|
94
100
|
this.emailAddresses = emailAddresses;
|
|
101
|
+
this.provisioner = provisioner;
|
|
95
102
|
this.emailAudit = new engine_1.MariaDbAuditSink(schemas ? { schema: schemas.audit } : {});
|
|
96
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Create a user; returns its id. Delegates to {@link createUserWithCredential} (same behaviour;
|
|
106
|
+
* the detailed credential outcome is dropped to keep this public signature stable).
|
|
107
|
+
*/
|
|
108
|
+
async createUser(actor, input) {
|
|
109
|
+
return (await this.createUserWithCredential(actor, input)).id;
|
|
110
|
+
}
|
|
97
111
|
/**
|
|
98
112
|
* Create a user. With `companyUuid`, the user is created AND attached (one active membership) in
|
|
99
113
|
* ONE transaction — the onboarding path for a non-root admin, whose membership create is checked
|
|
@@ -103,9 +117,13 @@ let UserService = UserService_1 = class UserService {
|
|
|
103
117
|
*
|
|
104
118
|
* KI #94: an `email` is stored canonical (the ONE address rule, as the email-change flow); a
|
|
105
119
|
* malformed one ⇒ {@link IdentityBadRequestError} (400) before any write; a blank one is no email.
|
|
106
|
-
*
|
|
120
|
+
*
|
|
121
|
+
* G-009 T7 (OD-6): with an email AND a company, a LOCKED credential (no password verifies; set
|
|
122
|
+
* later by reset) is created in that same transaction through the provisioner on the engine
|
|
123
|
+
* transaction's own connection; a provisioner failure rolls everything back. Otherwise no
|
|
124
|
+
* credential is created and the result says why (no email takes precedence).
|
|
107
125
|
*/
|
|
108
|
-
async
|
|
126
|
+
async createUserWithCredential(actor, input) {
|
|
109
127
|
const fields = { username: input.username };
|
|
110
128
|
const email = typeof input.email === 'string' ? (0, guard_1.canonicalEmail)(input.email) : '';
|
|
111
129
|
if (email !== '') {
|
|
@@ -113,17 +131,30 @@ let UserService = UserService_1 = class UserService {
|
|
|
113
131
|
throw new identity_errors_js_1.IdentityBadRequestError('Enter a valid email address.');
|
|
114
132
|
fields.email = email;
|
|
115
133
|
}
|
|
116
|
-
if (!input.companyUuid) {
|
|
117
|
-
return mappingDuplicates(() => this.records.create('user', { fields }));
|
|
118
|
-
}
|
|
119
134
|
const companyUuid = input.companyUuid;
|
|
120
|
-
|
|
121
|
-
const id = await
|
|
135
|
+
if (!companyUuid) {
|
|
136
|
+
const id = await mappingDuplicates(() => this.records.create('user', { fields }));
|
|
137
|
+
return { id, credential: 'none', reason: email === '' ? 'no_email' : 'no_company' };
|
|
138
|
+
}
|
|
139
|
+
const provisioner = this.provisioner;
|
|
140
|
+
const provision = email !== '' && provisioner != null;
|
|
141
|
+
const id = await mappingDuplicates(() => this.records.transaction(async (tx) => {
|
|
142
|
+
const created = await tx.create('user', { fields });
|
|
122
143
|
await tx.create('company_membership', {
|
|
123
|
-
fields: { userUuid:
|
|
144
|
+
fields: { userUuid: created, companyUuid, isActive: true, status: 'active' },
|
|
124
145
|
});
|
|
125
|
-
|
|
146
|
+
if (provision) {
|
|
147
|
+
await provisioner.provisionLocked({ principalId: created, hint: email, tenantId: companyUuid }, (0, internal_1.transactionQueryOf)(tx));
|
|
148
|
+
}
|
|
149
|
+
return created;
|
|
126
150
|
}));
|
|
151
|
+
if (provision)
|
|
152
|
+
return { id, credential: 'locked' };
|
|
153
|
+
return {
|
|
154
|
+
id,
|
|
155
|
+
credential: 'none',
|
|
156
|
+
reason: email === '' ? 'no_email' : 'no_provisioner',
|
|
157
|
+
};
|
|
127
158
|
}
|
|
128
159
|
/** List users (tenant-scoped by the pipe), with paging/search/sort. */
|
|
129
160
|
listUsers(params) {
|
|
@@ -296,6 +327,8 @@ exports.UserService = UserService = UserService_1 = __decorate([
|
|
|
296
327
|
__param(6, (0, common_1.Inject)(email_address_changer_token_js_1.IDENTITY_EMAIL_ADDRESS_CHANGER)),
|
|
297
328
|
__param(7, (0, common_1.Optional)()),
|
|
298
329
|
__param(7, (0, common_1.Inject)(framework_schemas_token_js_1.IDENTITY_FRAMEWORK_SCHEMAS)),
|
|
330
|
+
__param(8, (0, common_1.Optional)()),
|
|
331
|
+
__param(8, (0, common_1.Inject)(credential_provisioner_token_js_1.IDENTITY_CREDENTIAL_PROVISIONER)),
|
|
299
332
|
__metadata("design:paramtypes", [engine_1.RecordsService,
|
|
300
|
-
grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object, Object, Object])
|
|
333
|
+
grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object, Object, Object, Object])
|
|
301
334
|
], UserService);
|
package/dist/index.d.ts
CHANGED
|
@@ -40,6 +40,12 @@ export { buildIdentityAuthKeys, type IdentityAuthKeys } from './auth-facts-keys.
|
|
|
40
40
|
* `identityObjects`, and mounts the CRUD controllers — no separate engine setup needed.
|
|
41
41
|
*/
|
|
42
42
|
export { IdentityFeatureModule } from './features/identity-feature.module.js';
|
|
43
|
+
/**
|
|
44
|
+
* The org bootstrap (G-009): `runOrgBootstrap(app, { file, apply })` applies a declarative org file
|
|
45
|
+
* (roots, company tree, roles, users, postings) through the framework's own services, as the declared
|
|
46
|
+
* operation `shield:org-bootstrap`. Run at deploy time against the app's own `AppModule` context.
|
|
47
|
+
*/
|
|
48
|
+
export { runOrgBootstrap, OrgBootstrapError, type OrgBootstrapReport, type RunOrgBootstrapOptions, } from './features/org-bootstrap/run-org-bootstrap.js';
|
|
43
49
|
/**
|
|
44
50
|
* Stored files (file-storage SPEC §5 I-K): `store` / `open` / `remove` on the engine pipe, provided
|
|
45
51
|
* and exported by `IdentityFeatureModule`. Apps call it from their own `@Classified` routes.
|
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,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,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;;;;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"}
|