@endora-commerce/mod-customer-accounts 0.100.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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/admin/api/customer-groups-client.d.ts +27 -0
- package/dist/admin/api/customer-groups-client.d.ts.map +1 -0
- package/dist/admin/api/customer-groups-client.js +17 -0
- package/dist/admin/api/customer-groups-client.js.map +1 -0
- package/dist/admin/index.d.ts +33 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +38 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/CustomerGroupsPage.d.ts +18 -0
- package/dist/admin/pages/CustomerGroupsPage.d.ts.map +1 -0
- package/dist/admin/pages/CustomerGroupsPage.js +108 -0
- package/dist/admin/pages/CustomerGroupsPage.js.map +1 -0
- package/dist/backend/entities/customer-account.entity.d.ts +111 -0
- package/dist/backend/entities/customer-account.entity.d.ts.map +1 -0
- package/dist/backend/entities/customer-account.entity.js +230 -0
- package/dist/backend/entities/customer-account.entity.js.map +1 -0
- package/dist/backend/entities/customer-group.entity.d.ts +29 -0
- package/dist/backend/entities/customer-group.entity.d.ts.map +1 -0
- package/dist/backend/entities/customer-group.entity.js +70 -0
- package/dist/backend/entities/customer-group.entity.js.map +1 -0
- package/dist/backend/entities/password-reset-token.entity.d.ts +16 -0
- package/dist/backend/entities/password-reset-token.entity.d.ts.map +1 -0
- package/dist/backend/entities/password-reset-token.entity.js +59 -0
- package/dist/backend/entities/password-reset-token.entity.js.map +1 -0
- package/dist/backend/index.d.ts +144 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +407 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +26 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +35 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/services/audit-references.d.ts +22 -0
- package/dist/backend/services/audit-references.d.ts.map +1 -0
- package/dist/backend/services/audit-references.js +46 -0
- package/dist/backend/services/audit-references.js.map +1 -0
- package/dist/backend/services/customer-account-lifecycle-ports.d.ts +168 -0
- package/dist/backend/services/customer-account-lifecycle-ports.d.ts.map +1 -0
- package/dist/backend/services/customer-account-lifecycle-ports.js +420 -0
- package/dist/backend/services/customer-account-lifecycle-ports.js.map +1 -0
- package/dist/backend/services/customer-account-ports.d.ts +143 -0
- package/dist/backend/services/customer-account-ports.d.ts.map +1 -0
- package/dist/backend/services/customer-account-ports.js +422 -0
- package/dist/backend/services/customer-account-ports.js.map +1 -0
- package/dist/backend/services/customer-auth-service.d.ts +62 -0
- package/dist/backend/services/customer-auth-service.d.ts.map +1 -0
- package/dist/backend/services/customer-auth-service.js +105 -0
- package/dist/backend/services/customer-auth-service.js.map +1 -0
- package/dist/backend/services/customer-group-read-port.d.ts +24 -0
- package/dist/backend/services/customer-group-read-port.d.ts.map +1 -0
- package/dist/backend/services/customer-group-read-port.js +44 -0
- package/dist/backend/services/customer-group-read-port.js.map +1 -0
- package/dist/backend/services/customer-group-service.d.ts +21 -0
- package/dist/backend/services/customer-group-service.d.ts.map +1 -0
- package/dist/backend/services/customer-group-service.js +97 -0
- package/dist/backend/services/customer-group-service.js.map +1 -0
- package/dist/backend/services/customer-rollup-scope.d.ts +30 -0
- package/dist/backend/services/customer-rollup-scope.d.ts.map +1 -0
- package/dist/backend/services/customer-rollup-scope.js +42 -0
- package/dist/backend/services/customer-rollup-scope.js.map +1 -0
- package/dist/backend/services/password-reset-service.d.ts +15 -0
- package/dist/backend/services/password-reset-service.d.ts.map +1 -0
- package/dist/backend/services/password-reset-service.js +90 -0
- package/dist/backend/services/password-reset-service.js.map +1 -0
- package/dist/backend/services/role-service.d.ts +22 -0
- package/dist/backend/services/role-service.d.ts.map +1 -0
- package/dist/backend/services/role-service.js +83 -0
- package/dist/backend/services/role-service.js.map +1 -0
- package/dist/backend/services/two-factor-enrolments.d.ts +18 -0
- package/dist/backend/services/two-factor-enrolments.d.ts.map +1 -0
- package/dist/backend/services/two-factor-enrolments.js +2 -0
- package/dist/backend/services/two-factor-enrolments.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +212 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.d.ts +6 -0
- package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.d.ts.map +1 -0
- package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.js +25 -0
- package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.js.map +1 -0
- package/dist/migrations/20260611T140351_customer_accounts_organization_optional.d.ts +34 -0
- package/dist/migrations/20260611T140351_customer_accounts_organization_optional.d.ts.map +1 -0
- package/dist/migrations/20260611T140351_customer_accounts_organization_optional.js +43 -0
- package/dist/migrations/20260611T140351_customer_accounts_organization_optional.js.map +1 -0
- package/dist/migrations/20260611T140403_customer_accounts_lifecycle.d.ts +54 -0
- package/dist/migrations/20260611T140403_customer_accounts_lifecycle.d.ts.map +1 -0
- package/dist/migrations/20260611T140403_customer_accounts_lifecycle.js +97 -0
- package/dist/migrations/20260611T140403_customer_accounts_lifecycle.js.map +1 -0
- package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.d.ts +12 -0
- package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.d.ts.map +1 -0
- package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.js +16 -0
- package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.js.map +1 -0
- package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.d.ts +14 -0
- package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.d.ts.map +1 -0
- package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.js +18 -0
- package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.js.map +1 -0
- package/dist/migrations/20260819T074816_customer_accounts_password_set_at.d.ts +22 -0
- package/dist/migrations/20260819T074816_customer_accounts_password_set_at.d.ts.map +1 -0
- package/dist/migrations/20260819T074816_customer_accounts_password_set_at.js +26 -0
- package/dist/migrations/20260819T074816_customer_accounts_password_set_at.js.map +1 -0
- package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.d.ts +46 -0
- package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.d.ts.map +1 -0
- package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.js +97 -0
- package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.js.map +1 -0
- package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.d.ts +31 -0
- package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.d.ts.map +1 -0
- package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.js +35 -0
- package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.js.map +1 -0
- package/dist/migrations/20260825T141659_customer_accounts_organization_required.d.ts +58 -0
- package/dist/migrations/20260825T141659_customer_accounts_organization_required.d.ts.map +1 -0
- package/dist/migrations/20260825T141659_customer_accounts_organization_required.js +133 -0
- package/dist/migrations/20260825T141659_customer_accounts_organization_required.js.map +1 -0
- package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.d.ts +27 -0
- package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.d.ts.map +1 -0
- package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.js +44 -0
- package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.js.map +1 -0
- package/dist/migrations/index.d.ts +36 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +47 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/customer_accounts.md +38 -0
- package/i18n/en.json +30 -0
- package/i18n/pl.json +30 -0
- package/package.json +94 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { normalizeEmailAddress } from '@endora-commerce/contracts';
|
|
4
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
5
|
+
import { withSystemScope } from '@endora-commerce/platform/tenancy';
|
|
6
|
+
import { lazyPort } from '@endora-commerce/platform/kernel';
|
|
7
|
+
import { registerCustomerAccountAuditReferences } from './services/audit-references.js';
|
|
8
|
+
import { effectiveState } from '@endora-commerce/platform/kernel';
|
|
9
|
+
// Feature 075, Phase C — a pure function, so the kernel rather than `auth`.
|
|
10
|
+
import { hashPassword } from '@endora-commerce/platform/kernel';
|
|
11
|
+
import { CustomerAccount } from './entities/customer-account.entity.js';
|
|
12
|
+
import { CustomerGroup } from './entities/customer-group.entity.js';
|
|
13
|
+
import { PasswordResetToken } from './entities/password-reset-token.entity.js';
|
|
14
|
+
import { registerCustomerGroupAdminRoutes } from './routes.admin.js';
|
|
15
|
+
import { CustomerAccountMemberWriteService, CustomerAccountReadService, CustomerPasswordStateService, CustomerPasswordVerificationService, createCustomerAuthPort, createCustomerRolePort, } from './services/customer-account-ports.js';
|
|
16
|
+
import { CustomerAccountAdminSearchService, CustomerAccountLifecycleWriteService, } from './services/customer-account-lifecycle-ports.js';
|
|
17
|
+
import { CustomerAuthService } from './services/customer-auth-service.js';
|
|
18
|
+
import { CustomerGroupReadService } from './services/customer-group-read-port.js';
|
|
19
|
+
import { CustomerGroupService } from './services/customer-group-service.js';
|
|
20
|
+
import { CustomerRollupScopeService } from './services/customer-rollup-scope.js';
|
|
21
|
+
import { PasswordResetService } from './services/password-reset-service.js';
|
|
22
|
+
import { RoleService } from './services/role-service.js';
|
|
23
|
+
export function registerModule(ctx) {
|
|
24
|
+
/**
|
|
25
|
+
* Feature 120 (FR-015) — the sales-channel membership bridge this module owns.
|
|
26
|
+
*
|
|
27
|
+
* The platform used to hold a map **total** over
|
|
28
|
+
* `ChannelMemberEntityTypeSchema`, naming this module's bridge table from
|
|
29
|
+
* inside the kernel. That is D-52/D-53's prohibition expressed in a contract
|
|
30
|
+
* instead of an import, and it meant a membership call for 'customer' on an
|
|
31
|
+
* instance that never installed this module executed SQL against a relation
|
|
32
|
+
* that is not there (D-226). The module whose migration creates the table is
|
|
33
|
+
* the one that says where it is; a member nothing registers now refuses at
|
|
34
|
+
* the call instead.
|
|
35
|
+
*
|
|
36
|
+
* A **contribution** hook and deliberately **unprobed** (D-62/D-68): a
|
|
37
|
+
* registration is an inert statement about the schema rather than a
|
|
38
|
+
* capability, the rows outlive an operator switching this module off, and
|
|
39
|
+
* probing would make a runtime activation flip require a restart. It is
|
|
40
|
+
* declared first in this module so any later hook of its own that binds a
|
|
41
|
+
* membership finds the bridge already there.
|
|
42
|
+
*/
|
|
43
|
+
ctx.onBoot(() => {
|
|
44
|
+
const { salesChannelBridgeRegistry } = ctx.cradle();
|
|
45
|
+
for (const bridge of salesChannelBridges)
|
|
46
|
+
salesChannelBridgeRegistry.register(bridge);
|
|
47
|
+
});
|
|
48
|
+
// D-96 — resolved once, called through on every login. The gate `mfa`'s
|
|
49
|
+
// `providePort` put on this name is transient, so the proxy asks about the
|
|
50
|
+
// module's effective state at each call rather than at composition.
|
|
51
|
+
const mfaLoginPort = lazyPort(ctx, 'mfaLoginPort');
|
|
52
|
+
// The live source of `twoFactorEnabled`, on the same terms as the login port
|
|
53
|
+
// above and for the same reason. This module's `two_factor_confirmed_at`
|
|
54
|
+
// column lost its only non-null writer when the superseded TOTP path was
|
|
55
|
+
// deleted, so the field derived from it was a provably constant `false` —
|
|
56
|
+
// including on `GET /api/v1/me/customer`, which is the buyer's own account
|
|
57
|
+
// page telling them their account is unprotected while it is not.
|
|
58
|
+
const mfaEnrolmentStatePort = lazyPort(ctx, 'mfaEnrolmentStatePort');
|
|
59
|
+
/**
|
|
60
|
+
* The degrade is performed by **not resolving**, exactly as the login port's
|
|
61
|
+
* getter below does it — presence is decided first and there is deliberately
|
|
62
|
+
* no `catch` anywhere near the port. A buyer's own account page is the wrong
|
|
63
|
+
* place for a 503, and `false` is not a substitute for the answer here: with
|
|
64
|
+
* `mfa` absent no sign-in asks for a second factor, so no account is
|
|
65
|
+
* protected by one. The sentence an operator is shown before the flip is this
|
|
66
|
+
* module's `degrades-without` entry for `mfaEnrolmentStatePort`.
|
|
67
|
+
*/
|
|
68
|
+
const twoFactorEnrolments = async (customerAccountIds) => {
|
|
69
|
+
if (!effectiveState.isPresent('mfa'))
|
|
70
|
+
return new Set();
|
|
71
|
+
return new Set(await mfaEnrolmentStatePort.activeSubjectIds('customer', customerAccountIds));
|
|
72
|
+
};
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Feature 075, Phase P — the published surface.
|
|
75
|
+
//
|
|
76
|
+
// The three ports below this block already existed; what they lacked was a
|
|
77
|
+
// contract a consumer could name without naming a file in this directory.
|
|
78
|
+
// One of them (`passwordResetService`) already returns a plain shape, so it
|
|
79
|
+
// gains nothing but a type parameter, which is now the compile-time proof
|
|
80
|
+
// that it still satisfies what was published.
|
|
81
|
+
//
|
|
82
|
+
// The other two return the `CustomerAccount` **entity**, and an entity
|
|
83
|
+
// crossing a boundary is the problem this feature exists to remove — so they
|
|
84
|
+
// get record-returning siblings rather than a rename. `customerAuthPort` and
|
|
85
|
+
// `customerRolePort` are what the consumers rewired to — `customers` was the
|
|
86
|
+
// last of them, in issue #195 — and `customerAuthService` /
|
|
87
|
+
// `customerRoleService` stay registered because those two adapters are built
|
|
88
|
+
// over them.
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
ctx.di.providePort('customerAccountReadPort', ctx
|
|
91
|
+
.asFunction(({ emFactory }) => new CustomerAccountReadService(emFactory, twoFactorEnrolments))
|
|
92
|
+
.singleton());
|
|
93
|
+
/**
|
|
94
|
+
* Feature 075, Phase C — the member lifecycle `organizations` runs over this
|
|
95
|
+
* table. Published when that module was cut: it created and mutated the
|
|
96
|
+
* entity in seven places, and the argon2 hash it needed to do so came from an
|
|
97
|
+
* import of `auth`. Both are on this side of the port now, and each write
|
|
98
|
+
* records its own audit row, as `roleService` beside it always has.
|
|
99
|
+
*/
|
|
100
|
+
ctx.di.providePort('customerAccountMemberWritePort', ctx
|
|
101
|
+
.asFunction(({ emFactory, auditLogService }) => new CustomerAccountMemberWriteService(emFactory, twoFactorEnrolments, auditLogService))
|
|
102
|
+
.singleton());
|
|
103
|
+
/**
|
|
104
|
+
* Feature 075, Phase C — the account lifecycle `customers` runs over this
|
|
105
|
+
* table, and the list its admin screen reads it with.
|
|
106
|
+
*
|
|
107
|
+
* `customers` is the management surface over these accounts and it ran both
|
|
108
|
+
* by holding the entity: block, unblock, soft-delete, restore, the retention
|
|
109
|
+
* sweep's scrub, the organisation and group assignments, standalone
|
|
110
|
+
* registration and the custom-field patch, plus the paginated admin query.
|
|
111
|
+
* The policy — who may act on whom, and the "an organisation keeps an
|
|
112
|
+
* administrator" guard — stayed on that side; the statement and its one audit
|
|
113
|
+
* row are here.
|
|
114
|
+
*/
|
|
115
|
+
ctx.di.providePort('customerAccountLifecycleWritePort', ctx
|
|
116
|
+
.asFunction(({ emFactory, auditLogService, commandBus }) => new CustomerAccountLifecycleWriteService(emFactory, auditLogService, twoFactorEnrolments,
|
|
117
|
+
// D-178 — `lazyPort` rather than a captured value: the gate stays
|
|
118
|
+
// live inside this singleton and the seam is resolved per call. The
|
|
119
|
+
// proxy is what makes the capture legal at all.
|
|
120
|
+
lazyPort(ctx, 'personalOrganizationProvisionApi'), commandBus))
|
|
121
|
+
.singleton());
|
|
122
|
+
ctx.di.providePort('customerAccountAdminSearchPort', ctx
|
|
123
|
+
.asFunction(({ emFactory }) => new CustomerAccountAdminSearchService(emFactory, twoFactorEnrolments))
|
|
124
|
+
.singleton());
|
|
125
|
+
ctx.di.providePort('customerAuthPort', ctx
|
|
126
|
+
.asFunction(() => createCustomerAuthPort(() => ctx.cradle().customerAuthService, twoFactorEnrolments))
|
|
127
|
+
.singleton());
|
|
128
|
+
/**
|
|
129
|
+
* Issue #222 — whether a password its holder can use is on record. Read by
|
|
130
|
+
* `mfa` before it severs an account's last federated identity; nothing else
|
|
131
|
+
* needs it, which is why it is its own port rather than a field on
|
|
132
|
+
* `customerAccountReadPort`'s record.
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* Feature 080 (T040b) — the roll-up widening the per-request tenant-context
|
|
136
|
+
* builder asks about, published because both composition roots used to
|
|
137
|
+
* import the derivation out of this module's sources.
|
|
138
|
+
*
|
|
139
|
+
* The traversal arrives as an argument rather than being resolved here: the
|
|
140
|
+
* caller already holds `organizations`' tree service, and reading it from
|
|
141
|
+
* this side would be a cross-module resolution of a container name no
|
|
142
|
+
* contract publishes (`check:port-shape` signal 3).
|
|
143
|
+
*/
|
|
144
|
+
ctx.di.providePort('customerRollupScopePort', ctx
|
|
145
|
+
.asFunction(({ emFactory }) => new CustomerRollupScopeService(emFactory))
|
|
146
|
+
.singleton());
|
|
147
|
+
ctx.di.providePort('customerPasswordStatePort', ctx
|
|
148
|
+
.asFunction(({ emFactory }) => new CustomerPasswordStateService(emFactory))
|
|
149
|
+
.singleton());
|
|
150
|
+
/**
|
|
151
|
+
* Feature 080 (T052) — step-up re-verification, for `mfa`. Both composition
|
|
152
|
+
* roots used to read `passwordHash` off this module's entity and compare it
|
|
153
|
+
* with the platform hasher themselves; the column and the comparison are
|
|
154
|
+
* this module's, so the boolean is what crosses.
|
|
155
|
+
*
|
|
156
|
+
* Its own port rather than a method on `customerPasswordStatePort` for the
|
|
157
|
+
* reason that port gives for existing at all: the two questions differ, and
|
|
158
|
+
* only one of them takes a caller-supplied secret.
|
|
159
|
+
*/
|
|
160
|
+
ctx.di.providePort('customerPasswordVerificationPort', ctx
|
|
161
|
+
.asFunction(({ emFactory }) => new CustomerPasswordVerificationService(emFactory))
|
|
162
|
+
.singleton());
|
|
163
|
+
ctx.di.providePort('customerRolePort', ctx
|
|
164
|
+
.asFunction(() => createCustomerRolePort(() => ctx.cradle().customerRoleService, twoFactorEnrolments))
|
|
165
|
+
.singleton());
|
|
166
|
+
ctx.di.providePort('customerAuthService', ctx
|
|
167
|
+
.asFunction(({ emFactory, auditLogService }) => new CustomerAuthService(emFactory,
|
|
168
|
+
// Feature 075, Phase C — `auth`'s **published** session surface,
|
|
169
|
+
// where this used to resolve the `sessionService` registration and
|
|
170
|
+
// type itself against `auth`'s class. The port hands back a plain
|
|
171
|
+
// cookie payload; the `Session` entity no longer crosses.
|
|
172
|
+
lazyPort(ctx, 'authSessionPort'),
|
|
173
|
+
// D-96 — the degrade is performed by **not resolving**. The port's
|
|
174
|
+
// gate throws `ModuleDisabledError` when `mfa` is absent, and there
|
|
175
|
+
// is deliberately no `catch` anywhere near this: a caught gate is a
|
|
176
|
+
// fail-open degrade nobody declared. Deciding presence first is the
|
|
177
|
+
// `auth`/`api_keys` shape, and `CustomerAuthService`'s
|
|
178
|
+
// `if (mfaPort)` branch — the password-only fallback of feature 042
|
|
179
|
+
// FR-033 — is what `undefined` selects.
|
|
180
|
+
() => (effectiveState.isPresent('mfa') ? mfaLoginPort : undefined), auditLogService))
|
|
181
|
+
.singleton());
|
|
182
|
+
// The type parameter is the compile-time proof that this service still
|
|
183
|
+
// satisfies what feature 075 published; it returns plain shapes already, so
|
|
184
|
+
// it needed no adapter.
|
|
185
|
+
ctx.di.providePort('passwordResetService', ctx
|
|
186
|
+
.asFunction(({ emFactory, auditLogService }) => new PasswordResetService(emFactory, auditLogService))
|
|
187
|
+
.singleton());
|
|
188
|
+
ctx.di.providePort('customerRoleService', ctx
|
|
189
|
+
.asFunction(({ emFactory, auditLogService }) => new RoleService(emFactory, auditLogService))
|
|
190
|
+
.singleton());
|
|
191
|
+
ctx.di.providePort('customerOrgResolver', ctx
|
|
192
|
+
.asFunction(({ emFactory }) => async (customerAccountId) => {
|
|
193
|
+
// Runs in the auth hook, before a tenant context exists, so the
|
|
194
|
+
// read is system-scoped — identity resolution precedes tenancy.
|
|
195
|
+
return withSystemScope('auth: resolve customer org', async () => {
|
|
196
|
+
const customer = await emFactory().findOne(CustomerAccount, {
|
|
197
|
+
id: customerAccountId,
|
|
198
|
+
});
|
|
199
|
+
return customer?.organizationId ?? null;
|
|
200
|
+
});
|
|
201
|
+
})
|
|
202
|
+
.singleton());
|
|
203
|
+
/**
|
|
204
|
+
* Federated sign-in's two reads/writes of this module's table (T143a).
|
|
205
|
+
*
|
|
206
|
+
* A **port** rather than a contribution point (D-39): `autoCreate` decides
|
|
207
|
+
* whether an account may exist at all and then writes one. Both halves are
|
|
208
|
+
* system-scoped because they run *before* a tenant context exists — identity
|
|
209
|
+
* resolution precedes tenancy, exactly as `customerOrgResolver` above.
|
|
210
|
+
*
|
|
211
|
+
* The refusal is the return type rather than a throw: `null` is what
|
|
212
|
+
* `SocialIdentityService` reads as `registration_required`, so the policy
|
|
213
|
+
* lands on the caller as a redirect to the login screen and not as a 500.
|
|
214
|
+
*
|
|
215
|
+
* The setting is `customers.allow_registration_without_organization`, and it
|
|
216
|
+
* is named here even though `customers` owns it. That module already depends
|
|
217
|
+
* on this one, so a port pointing the other way would close a cycle; the
|
|
218
|
+
* platform-wide question "may an account exist without an Organization" is in
|
|
219
|
+
* any case about *this* table.
|
|
220
|
+
*/
|
|
221
|
+
ctx.di.providePort('customerSocialLoginPort', ctx
|
|
222
|
+
.asFunction(({ emFactory, auditLogService }) => ({
|
|
223
|
+
async resolveByEmail(email) {
|
|
224
|
+
return withSystemScope('mfa: resolve customer by email', async () => {
|
|
225
|
+
// The address arrives from the provider's claim, in whatever case
|
|
226
|
+
// that provider chose to send it; the row is folded, so the compare
|
|
227
|
+
// is too. Unfolded, a returning buyer would be offered registration
|
|
228
|
+
// for an account they already have — and `autoCreate` below would
|
|
229
|
+
// then be refused by the unique index.
|
|
230
|
+
const customer = await emFactory().findOne(CustomerAccount, {
|
|
231
|
+
email: normalizeEmailAddress(email),
|
|
232
|
+
deletedAt: null,
|
|
233
|
+
});
|
|
234
|
+
return customer ? { id: customer.id } : null;
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
async autoCreate(email) {
|
|
238
|
+
const cradle = ctx.cradle();
|
|
239
|
+
// The channel a global-scope read resolves against. Both roots used
|
|
240
|
+
// to pass the literal `'default'` here, which is **not** a channel
|
|
241
|
+
// id: `sales_channel_id` is a uuid column, so the query threw
|
|
242
|
+
// `invalid input syntax for type uuid` on every call, the closure's
|
|
243
|
+
// `catch` read that as "not allowed", and federated sign-in could
|
|
244
|
+
// therefore never create an account in production however the
|
|
245
|
+
// operator had configured it. The harness had no gate at all and
|
|
246
|
+
// always created one. Neither was the behaviour anybody wanted.
|
|
247
|
+
const salesChannelId = await cradle.settingsChannelResolver();
|
|
248
|
+
if (salesChannelId === null)
|
|
249
|
+
return null;
|
|
250
|
+
let allowed = false;
|
|
251
|
+
try {
|
|
252
|
+
allowed = await cradle.settingsReadPort.get('customers.allow_registration_without_organization', salesChannelId, z.boolean());
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// An unregistered or out-of-scope setting denies. Not a bare catch
|
|
256
|
+
// around a module port — `settingsReadPort` is kernel-owned and
|
|
257
|
+
// ungated, so there is no `ModuleDisabledError` to swallow here;
|
|
258
|
+
// the degrade is "no answer means no", which is the safe direction
|
|
259
|
+
// for a gate on account creation.
|
|
260
|
+
allowed = false;
|
|
261
|
+
}
|
|
262
|
+
if (!allowed)
|
|
263
|
+
return null;
|
|
264
|
+
const folded = normalizeEmailAddress(email);
|
|
265
|
+
const customerAccountId = randomUUID();
|
|
266
|
+
// No password was ever chosen for this account: it signs in through
|
|
267
|
+
// the provider. A random one keeps the column non-null without minting
|
|
268
|
+
// a credential anybody could guess — and issue #222 is the other half
|
|
269
|
+
// of that sentence: `passwordSetAt` stays null, so a reader can tell
|
|
270
|
+
// this account from one whose holder really has a password. Do not
|
|
271
|
+
// stamp it here.
|
|
272
|
+
const passwordHash = await hashPassword(randomUUID() + randomUUID());
|
|
273
|
+
const provision = lazyPort(ctx, 'personalOrganizationProvisionApi');
|
|
274
|
+
return withSystemScope('mfa: auto-create customer from social login', async () => emFactory().transactional(async (em) => {
|
|
275
|
+
/**
|
|
276
|
+
* D-178 — the personal organisation, in the same transaction as
|
|
277
|
+
* the account.
|
|
278
|
+
*
|
|
279
|
+
* This path provisioned **none**, so every account a federated
|
|
280
|
+
* sign-in created carried `organization_id = NULL` permanently:
|
|
281
|
+
* not a window but the steady state of a shipped feature, and the
|
|
282
|
+
* buyer reaching checkout was told to attach an Organization they
|
|
283
|
+
* had no way to attach. `customers`' self-registration provisioned
|
|
284
|
+
* one in a second unit of work; this one is a single transaction,
|
|
285
|
+
* which is what closes the window that shape leaves.
|
|
286
|
+
*
|
|
287
|
+
* The provider sends no name with the claim this seam reads, so
|
|
288
|
+
* the organisation is named from the address' local part — which
|
|
289
|
+
* is exactly what `PersonalOrganizationService.personalName` falls
|
|
290
|
+
* back to for a nameless account.
|
|
291
|
+
*/
|
|
292
|
+
const organization = await provision.provisionFor(em, {
|
|
293
|
+
customerAccountId,
|
|
294
|
+
email: folded,
|
|
295
|
+
});
|
|
296
|
+
const account = em.create(CustomerAccount, {
|
|
297
|
+
id: customerAccountId,
|
|
298
|
+
email: folded,
|
|
299
|
+
passwordHash,
|
|
300
|
+
passwordSetAt: null,
|
|
301
|
+
firstName: '',
|
|
302
|
+
lastName: '',
|
|
303
|
+
role: 'regular_user',
|
|
304
|
+
organizationId: organization.id,
|
|
305
|
+
emailVerifiedAt: new Date(),
|
|
306
|
+
});
|
|
307
|
+
// The other way an account is created without an admin —
|
|
308
|
+
// `customers`' standalone self-registration — records this same
|
|
309
|
+
// shape with a null actor, and this path did not record anything at
|
|
310
|
+
// all until the coverage scan reached `backend.ts` (issue #122). An
|
|
311
|
+
// account appearing out of a federated sign-in is exactly the event
|
|
312
|
+
// an operator later needs to explain.
|
|
313
|
+
recordAuditFromContext(auditLogService, em, {
|
|
314
|
+
action: 'customer_account.register_social',
|
|
315
|
+
objectType: 'customer_account',
|
|
316
|
+
objectId: account.id,
|
|
317
|
+
stateBefore: null,
|
|
318
|
+
stateAfter: { email: account.email },
|
|
319
|
+
});
|
|
320
|
+
await em.flush();
|
|
321
|
+
return { id: account.id };
|
|
322
|
+
}));
|
|
323
|
+
},
|
|
324
|
+
}))
|
|
325
|
+
.singleton());
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// Feature 076, D-79 — customer groups.
|
|
328
|
+
//
|
|
329
|
+
// The entity, its service, its read port and its three admin routes moved
|
|
330
|
+
// here from `price_lists`. A customer group describes the customer; a price
|
|
331
|
+
// list refers to one by id. Under the previous owner the single real foreign
|
|
332
|
+
// key into `customer_groups` — `customer_accounts.customer_group_id` — forced
|
|
333
|
+
// this module to declare `price_lists`, which made the reverse declaration a
|
|
334
|
+
// cycle `test/unit/db/module-graph.test.ts` refuses. The key is intra-module now and
|
|
335
|
+
// the class of problem is gone rather than routed around.
|
|
336
|
+
//
|
|
337
|
+
// `customerGroupService` keeps its container name: both composition roots
|
|
338
|
+
// read it for the promotion Rule Builder's target picker, and renaming it
|
|
339
|
+
// would have churned two roots to encode an owner this file already states.
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
ctx.di.providePort('customerGroupService', ctx
|
|
342
|
+
.asFunction(({ emFactory, commandBus }) => new CustomerGroupService(emFactory, commandBus))
|
|
343
|
+
.singleton());
|
|
344
|
+
ctx.di.providePort('customerGroupReadPort', ctx
|
|
345
|
+
.asFunction(({ emFactory }) => new CustomerGroupReadService(emFactory))
|
|
346
|
+
.singleton());
|
|
347
|
+
// The module's only routes. They are the customer-group admin surface and
|
|
348
|
+
// nothing else — customer login, registration and self-service stay with
|
|
349
|
+
// `organizations` and `customers`, which own those screens.
|
|
350
|
+
ctx.routes(async (app) => {
|
|
351
|
+
await registerCustomerGroupAdminRoutes(app, {
|
|
352
|
+
customerGroupService: () => ctx.cradle().customerGroupService,
|
|
353
|
+
requireAdmin: (permission) => async (req, reply) => ctx.cradle().requireAdmin(permission)(req, reply),
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
/**
|
|
357
|
+
* What an audit row about an impersonated customer is called, and where the admin app shows it
|
|
358
|
+
* (feature 075, D-87 drain).
|
|
359
|
+
*
|
|
360
|
+
* `audit_logs` used to answer both by hand — one SQL statement naming this
|
|
361
|
+
* module's table, and this module's admin route spelled into its own switch.
|
|
362
|
+
* A read port would have been the wrong repair: `audit_logs` is a
|
|
363
|
+
* cross-cutting reader, and five ports into it would be five edges pointing
|
|
364
|
+
* from the record towards the things it records. One of the five contributors
|
|
365
|
+
* (`inventory`) is switchable, and `audit_logs` is `nonDeactivatable`, so that
|
|
366
|
+
* edge would also have taken the operator's switch away. A push costs nothing
|
|
367
|
+
* and reads the same for all five.
|
|
368
|
+
*
|
|
369
|
+
* A **contribution** hook: it pushes an inert resolver into
|
|
370
|
+
* `auditReferenceRegistry`, an ungated registry, and carries no presence probe
|
|
371
|
+
* (D-67/D-68). The registry's own enumeration policy is what drops this entry
|
|
372
|
+
* while the module is absent — probing here would make the drop survive a
|
|
373
|
+
* reactivation until the next restart.
|
|
374
|
+
*/
|
|
375
|
+
ctx.onBoot(() => {
|
|
376
|
+
registerCustomerAccountAuditReferences(lazyPort(ctx, 'auditReferenceRegistry'), ctx.cradle().emFactory, lazyPort(ctx, 'organizationDetailsPort'));
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The module's persisted entity classes, on the `./backend` subpath, as one
|
|
381
|
+
* array and **no named class export** (D-168).
|
|
382
|
+
*
|
|
383
|
+
* This is the shape the platform reads when the package is *installed*: the
|
|
384
|
+
* boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
|
|
385
|
+
* and the static declaration reader (`scripts/lib/package-declarations.ts`),
|
|
386
|
+
* which is the third source of `check:module-boundary`'s `table->owner` map and
|
|
387
|
+
* the package pass of `check-entity-tenant-classification`. A missing array is
|
|
388
|
+
* answered with `[]` — zero entities registered, no error anywhere.
|
|
389
|
+
*
|
|
390
|
+
* The order is the one `db/entities-registry.generated.ts` declared before this
|
|
391
|
+
* module became a package, so the registered set is the same list in the same
|
|
392
|
+
* sequence.
|
|
393
|
+
*/
|
|
394
|
+
export const entities = [CustomerAccount, CustomerGroup, PasswordResetToken];
|
|
395
|
+
/**
|
|
396
|
+
* The sales-channel membership bridge this module owns (feature 120, FR-015).
|
|
397
|
+
*
|
|
398
|
+
* Exported as well as registered because the owning module is the only honest
|
|
399
|
+
* source for it: a caller that composes no platform — the bare-database test
|
|
400
|
+
* harness, which constructs the kernel's membership service directly — declares
|
|
401
|
+
* these from here rather than from a list of nine it keeps itself, which is
|
|
402
|
+
* exactly the total map this feature deleted.
|
|
403
|
+
*/
|
|
404
|
+
export const salesChannelBridges = [
|
|
405
|
+
{ entityType: 'customer', table: 'sales_channel_customer_accounts', entityIdColumn: 'customer_account_id' },
|
|
406
|
+
];
|
|
407
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import type { CustomerGroupService } from './services/customer-group-service.js';
|
|
4
|
+
/**
|
|
5
|
+
* The customer-group admin surface (feature 076, D-79).
|
|
6
|
+
*
|
|
7
|
+
* Three routes, at the paths they have always had — `price_lists` served them
|
|
8
|
+
* until this module took the entity over, and the admin SPA's pickers call the
|
|
9
|
+
* same URLs with the same payloads. What changed is the gate: `catalog:write`
|
|
10
|
+
* was the pricing module's permission and is the wrong question to ask about a
|
|
11
|
+
* customer's segmentation, so the surface is gated by the two codes this module
|
|
12
|
+
* declares in its manifest. An operator's role has to be granted them; there is
|
|
13
|
+
* no silent inheritance from `catalog:write`.
|
|
14
|
+
*/
|
|
15
|
+
export interface CustomerGroupAdminDeps {
|
|
16
|
+
/**
|
|
17
|
+
* Resolved per call rather than captured: the registration is a gated port,
|
|
18
|
+
* and a gate captured at boot keeps answering whatever the effective state
|
|
19
|
+
* later says. The module is non-deactivatable, so the gate is unreachable —
|
|
20
|
+
* which is a reason to be uniform, not a reason to be careless.
|
|
21
|
+
*/
|
|
22
|
+
customerGroupService: () => CustomerGroupService;
|
|
23
|
+
requireAdmin: RequireAdminFactory;
|
|
24
|
+
}
|
|
25
|
+
export declare function registerCustomerGroupAdminRoutes(app: FastifyInstance, deps: CustomerGroupAdminDeps): Promise<void>;
|
|
26
|
+
//# sourceMappingURL=routes.admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAEjF;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;IACjD,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAsB,gCAAgC,CACpD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAqCf"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { upsertCustomerGroupRequestSchema } from '@endora-commerce/contracts';
|
|
2
|
+
export async function registerCustomerGroupAdminRoutes(app, deps) {
|
|
3
|
+
const { customerGroupService, requireAdmin } = deps;
|
|
4
|
+
app.get('/api/v1/admin/customer-groups', { preHandler: requireAdmin('customer_groups:read') }, async () => {
|
|
5
|
+
const rows = await customerGroupService().list();
|
|
6
|
+
return { data: rows.map(serializeCustomerGroup) };
|
|
7
|
+
});
|
|
8
|
+
app.put('/api/v1/admin/customer-groups/:code', {
|
|
9
|
+
preHandler: requireAdmin('customer_groups:write'),
|
|
10
|
+
schema: { body: upsertCustomerGroupRequestSchema },
|
|
11
|
+
}, async (request) => {
|
|
12
|
+
const body = upsertCustomerGroupRequestSchema.parse(request.body);
|
|
13
|
+
const row = await customerGroupService().upsertByCode({
|
|
14
|
+
code: request.params.code,
|
|
15
|
+
name: body.name,
|
|
16
|
+
...(body.description !== undefined ? { description: body.description } : {}),
|
|
17
|
+
});
|
|
18
|
+
return { data: serializeCustomerGroup(row) };
|
|
19
|
+
});
|
|
20
|
+
app.delete('/api/v1/admin/customer-groups/:id', { preHandler: requireAdmin('customer_groups:write') }, async (request, reply) => {
|
|
21
|
+
await customerGroupService().remove(request.params.id);
|
|
22
|
+
return reply.status(204).send();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function serializeCustomerGroup(row) {
|
|
26
|
+
return {
|
|
27
|
+
id: row.id,
|
|
28
|
+
code: row.code,
|
|
29
|
+
name: row.name,
|
|
30
|
+
description: row.description ?? null,
|
|
31
|
+
createdAt: row.createdAt.toISOString(),
|
|
32
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=routes.admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.js","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AA2B9E,MAAM,CAAC,KAAK,UAAU,gCAAgC,CACpD,GAAoB,EACpB,IAA4B;IAE5B,MAAM,EAAE,oBAAoB,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAEpD,GAAG,CAAC,GAAG,CACL,+BAA+B,EAC/B,EAAE,UAAU,EAAE,YAAY,CAAC,sBAAsB,CAAC,EAAE,EACpD,KAAK,IAAI,EAAE;QACT,MAAM,IAAI,GAAG,MAAM,oBAAoB,EAAE,CAAC,IAAI,EAAE,CAAC;QACjD,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,sBAAsB,CAAC,EAAE,CAAC;IACpD,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,qCAAqC,EACrC;QACE,UAAU,EAAE,YAAY,CAAC,uBAAuB,CAAC;QACjD,MAAM,EAAE,EAAE,IAAI,EAAE,gCAAgC,EAAE;KACnD,EACD,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,gCAAgC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAClE,MAAM,GAAG,GAAG,MAAM,oBAAoB,EAAE,CAAC,YAAY,CAAC;YACpD,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI;YACzB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,GAAG,CAAC,IAAI,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC7E,CAAC,CAAC;QACH,OAAO,EAAE,IAAI,EAAE,sBAAsB,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/C,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,MAAM,CACR,mCAAmC,EACnC,EAAE,UAAU,EAAE,YAAY,CAAC,uBAAuB,CAAC,EAAE,EACrD,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,oBAAoB,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACvD,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,CAAC,CACF,CAAC;AACJ,CAAC;AAED,SAAS,sBAAsB,CAAC,GAAkB;IAChD,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;QACpC,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;QACtC,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;KACvC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AuditReferenceRegistryPort, OrganizationDetailsPort } from '@endora-commerce/contracts';
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
3
|
+
/**
|
|
4
|
+
* What an impersonated customer is called on an audit row (feature 075, D-87
|
|
5
|
+
* drain).
|
|
6
|
+
*
|
|
7
|
+
* `audit_logs` used to answer this with a `customer_accounts left join
|
|
8
|
+
* organizations` written into its own service — one statement crossing two
|
|
9
|
+
* module boundaries. The organisation name is still preferred over the account
|
|
10
|
+
* e-mail, because that is what an operator reading "acted as …" recognises; it
|
|
11
|
+
* comes from `organizationDetailsPort` now, an edge this module already declares
|
|
12
|
+
* and has always had a foreign key for.
|
|
13
|
+
*
|
|
14
|
+
* There is no `catch` around that port on purpose. It is gated, and a gate that
|
|
15
|
+
* says no must not be turned into a blank name here — the reader's own
|
|
16
|
+
* enumeration policy is what degrades this whole bucket, one level up.
|
|
17
|
+
*
|
|
18
|
+
* No `url`: an audit actor is a person, not a row the card links to. That was
|
|
19
|
+
* already true of the SQL this replaces.
|
|
20
|
+
*/
|
|
21
|
+
export declare function registerCustomerAccountAuditReferences(registry: AuditReferenceRegistryPort, emFactory: () => EntityManager, organizationDetails: OrganizationDetailsPort): void;
|
|
22
|
+
//# sourceMappingURL=audit-references.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-references.d.ts","sourceRoot":"","sources":["../../../src/backend/services/audit-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACtG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sCAAsC,CACpD,QAAQ,EAAE,0BAA0B,EACpC,SAAS,EAAE,MAAM,aAAa,EAC9B,mBAAmB,EAAE,uBAAuB,GAC3C,IAAI,CA+BN"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { CustomerAccount } from '../entities/customer-account.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* What an impersonated customer is called on an audit row (feature 075, D-87
|
|
4
|
+
* drain).
|
|
5
|
+
*
|
|
6
|
+
* `audit_logs` used to answer this with a `customer_accounts left join
|
|
7
|
+
* organizations` written into its own service — one statement crossing two
|
|
8
|
+
* module boundaries. The organisation name is still preferred over the account
|
|
9
|
+
* e-mail, because that is what an operator reading "acted as …" recognises; it
|
|
10
|
+
* comes from `organizationDetailsPort` now, an edge this module already declares
|
|
11
|
+
* and has always had a foreign key for.
|
|
12
|
+
*
|
|
13
|
+
* There is no `catch` around that port on purpose. It is gated, and a gate that
|
|
14
|
+
* says no must not be turned into a blank name here — the reader's own
|
|
15
|
+
* enumeration policy is what degrades this whole bucket, one level up.
|
|
16
|
+
*
|
|
17
|
+
* No `url`: an audit actor is a person, not a row the card links to. That was
|
|
18
|
+
* already true of the SQL this replaces.
|
|
19
|
+
*/
|
|
20
|
+
export function registerCustomerAccountAuditReferences(registry, emFactory, organizationDetails) {
|
|
21
|
+
registry.register({
|
|
22
|
+
ownerModuleId: 'customer_accounts',
|
|
23
|
+
referenceType: 'customer_account',
|
|
24
|
+
resolve: async (ids) => {
|
|
25
|
+
const accounts = await emFactory().find(CustomerAccount, { id: { $in: [...ids] } }, { fields: ['id', 'email', 'organizationId'] });
|
|
26
|
+
if (accounts.length === 0)
|
|
27
|
+
return [];
|
|
28
|
+
const organizationIds = [
|
|
29
|
+
...new Set(accounts.map((a) => a.organizationId).filter((id) => !!id)),
|
|
30
|
+
];
|
|
31
|
+
const names = new Map();
|
|
32
|
+
if (organizationIds.length > 0) {
|
|
33
|
+
for (const org of await organizationDetails.findByIds(organizationIds)) {
|
|
34
|
+
names.set(org.id, org.name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return accounts.map((account) => ({
|
|
38
|
+
id: account.id,
|
|
39
|
+
label: (account.organizationId ? names.get(account.organizationId) : undefined) ??
|
|
40
|
+
account.email,
|
|
41
|
+
url: null,
|
|
42
|
+
}));
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=audit-references.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-references.js","sourceRoot":"","sources":["../../../src/backend/services/audit-references.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAEzE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,sCAAsC,CACpD,QAAoC,EACpC,SAA8B,EAC9B,mBAA4C;IAE5C,QAAQ,CAAC,QAAQ,CAAC;QAChB,aAAa,EAAE,mBAAmB;QAClC,aAAa,EAAE,kBAAkB;QACjC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACrB,MAAM,QAAQ,GAAG,MAAM,SAAS,EAAE,CAAC,IAAI,CACrC,eAAe,EACf,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,EAAE,EACzB,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,gBAAgB,CAAC,EAAE,CAC9C,CAAC;YACF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YAErC,MAAM,eAAe,GAAG;gBACtB,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;aACrF,CAAC;YACF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;YACxC,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC/B,KAAK,MAAM,GAAG,IAAI,MAAM,mBAAmB,CAAC,SAAS,CAAC,eAAe,CAAC,EAAE,CAAC;oBACvE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;gBAC9B,CAAC;YACH,CAAC;YAED,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBAChC,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,KAAK,EACH,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;oBACxE,OAAO,CAAC,KAAK;gBACf,GAAG,EAAE,IAAI;aACV,CAAC,CAAC,CAAC;QACN,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|