@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,143 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type CustomerAccountCreateInput, type CustomerAccountLookupOptions, type CustomerAccountMemberWritePort, type CustomerAccountProfilePatch, type CustomerAccountReadPort, type CustomerAccountRecord, type CustomerAccountRole, type CustomerAuthPort, type CustomerPasswordStatePort, type CustomerPasswordVerificationPort, type CustomerRolePort } from '@endora-commerce/contracts';
|
|
3
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { CustomerAccount } from '../entities/customer-account.entity.js';
|
|
5
|
+
import type { CustomerAuthService } from './customer-auth-service.js';
|
|
6
|
+
import type { RoleService } from './role-service.js';
|
|
7
|
+
import type { TwoFactorEnrolmentReader } from './two-factor-enrolments.js';
|
|
8
|
+
/**
|
|
9
|
+
* The published face of `customer_accounts` (feature 075, Phase P).
|
|
10
|
+
*
|
|
11
|
+
* Nineteen modules read this module's table today, 51 of the sites on the
|
|
12
|
+
* entity class itself. What they ask for is narrow — an account by id, by ids,
|
|
13
|
+
* by e-mail, by organisation, and the two counting questions the "an
|
|
14
|
+
* organisation keeps at least one admin" rule is spelled with — so that is
|
|
15
|
+
* what the read port is, rather than the table.
|
|
16
|
+
*
|
|
17
|
+
* The mapping below is the point of the exercise and not ceremony:
|
|
18
|
+
* `passwordHash` does not survive it. It is read by exactly one module, and a
|
|
19
|
+
* record that carried it would turn every consumer into a place a credential
|
|
20
|
+
* can leak from.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The published record, with `twoFactorEnabled` taken from `mfa` rather than
|
|
24
|
+
* from this module's `two_factor_confirmed_at` column.
|
|
25
|
+
*
|
|
26
|
+
* The second parameter is required and has no default on purpose. That column's
|
|
27
|
+
* only non-null writer was the superseded TOTP path deleted on 2026-08-25, so
|
|
28
|
+
* the field was a **provably constant `false`** — on the admin customer detail,
|
|
29
|
+
* on both organisation member panels, and on `GET /api/v1/me/customer`, which
|
|
30
|
+
* is the buyer's own account page telling them their account is unprotected. A
|
|
31
|
+
* default would let the next call site reintroduce that silently; making the
|
|
32
|
+
* caller answer means the compiler asks.
|
|
33
|
+
*/
|
|
34
|
+
export declare function toCustomerAccountRecord(account: CustomerAccount, twoFactorEnabled: boolean): CustomerAccountRecord;
|
|
35
|
+
/**
|
|
36
|
+
* One `mfa` read for a whole page of accounts, rather than one per row.
|
|
37
|
+
*
|
|
38
|
+
* Every caller is a list or a single row, so batching here keeps the admin
|
|
39
|
+
* customer list at two queries instead of one per customer, and leaves the
|
|
40
|
+
* single-row callers spelling the same thing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function toCustomerAccountRecords(twoFactorEnrolments: TwoFactorEnrolmentReader, accounts: readonly CustomerAccount[]): Promise<CustomerAccountRecord[]>;
|
|
43
|
+
/** The single-row spelling of {@link toCustomerAccountRecords}. */
|
|
44
|
+
export declare function toOneCustomerAccountRecord(twoFactorEnrolments: TwoFactorEnrolmentReader, account: CustomerAccount): Promise<CustomerAccountRecord>;
|
|
45
|
+
export declare class CustomerAccountReadService implements CustomerAccountReadPort {
|
|
46
|
+
private readonly emFactory;
|
|
47
|
+
private readonly twoFactorEnrolments;
|
|
48
|
+
constructor(emFactory: () => EntityManager, twoFactorEnrolments: TwoFactorEnrolmentReader);
|
|
49
|
+
findById(id: string, options?: CustomerAccountLookupOptions): Promise<CustomerAccountRecord | null>;
|
|
50
|
+
findByIds(ids: readonly string[], options?: CustomerAccountLookupOptions): Promise<CustomerAccountRecord[]>;
|
|
51
|
+
findByEmail(email: string, options?: CustomerAccountLookupOptions): Promise<CustomerAccountRecord | null>;
|
|
52
|
+
findInOrganization(id: string, organizationId: string, options?: CustomerAccountLookupOptions): Promise<CustomerAccountRecord | null>;
|
|
53
|
+
listByOrganization(organizationId: string, options?: CustomerAccountLookupOptions): Promise<CustomerAccountRecord[]>;
|
|
54
|
+
countByOrganizationRole(organizationId: string, role: CustomerAccountRole, options?: {
|
|
55
|
+
excludeCustomerAccountId?: string;
|
|
56
|
+
activeOnly?: boolean;
|
|
57
|
+
notBlocked?: boolean;
|
|
58
|
+
}): Promise<number>;
|
|
59
|
+
searchByEmail(query: string, limit: number): Promise<CustomerAccountRecord[]>;
|
|
60
|
+
searchIdsByName(query: string): Promise<string[]>;
|
|
61
|
+
listAll(): Promise<CustomerAccountRecord[]>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Issue #222 — the one credential fact that crosses this boundary, and it
|
|
65
|
+
* crosses as a date rather than as anything derived from the hash.
|
|
66
|
+
*
|
|
67
|
+
* Separate from {@link CustomerAccountReadService} on purpose: that read port
|
|
68
|
+
* is what nineteen modules resolve, and the mapping above deliberately drops
|
|
69
|
+
* `passwordHash`. "Does this account have a password its holder can use" is a
|
|
70
|
+
* question one module asks, so it gets a port one module resolves.
|
|
71
|
+
*
|
|
72
|
+
* An unknown id answers `null`, not a throw: the caller's question is about a
|
|
73
|
+
* credential, and "no account" and "no password on record" want the same
|
|
74
|
+
* refusal from every caller there could be.
|
|
75
|
+
*/
|
|
76
|
+
export declare class CustomerPasswordStateService implements CustomerPasswordStatePort {
|
|
77
|
+
private readonly emFactory;
|
|
78
|
+
constructor(emFactory: () => EntityManager);
|
|
79
|
+
passwordSetAt(customerAccountId: string): Promise<Date | null>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Step-up re-verification, for `mfa` (feature 080, T052).
|
|
83
|
+
*
|
|
84
|
+
* The read is this module's own row and the comparison is the platform
|
|
85
|
+
* hasher's, so both halves are on this side of the seam. The composition roots
|
|
86
|
+
* held them instead: each read `passwordHash` off the entity and called
|
|
87
|
+
* `verifyPassword` itself, which is a credential column and a hash comparison
|
|
88
|
+
* living in a file that owns neither.
|
|
89
|
+
*
|
|
90
|
+
* The hash never leaves this class — that is the difference between this port
|
|
91
|
+
* and a `passwordHash` field on `CustomerAccountRecord`, and it is the whole
|
|
92
|
+
* reason the port exists.
|
|
93
|
+
*/
|
|
94
|
+
export declare class CustomerPasswordVerificationService implements CustomerPasswordVerificationPort {
|
|
95
|
+
private readonly emFactory;
|
|
96
|
+
constructor(emFactory: () => EntityManager);
|
|
97
|
+
verifyPassword(customerAccountId: string, password: string): Promise<boolean>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The member lifecycle `organizations` runs over this module's table
|
|
101
|
+
* (feature 075, Phase C).
|
|
102
|
+
*
|
|
103
|
+
* It ran it by creating and mutating the `CustomerAccount` entity in its own
|
|
104
|
+
* services and route handlers — seven write sites across five files. They are
|
|
105
|
+
* six methods here, each one unit of work on this table alone, because a write
|
|
106
|
+
* port that took the caller's `EntityManager` would publish the coupling
|
|
107
|
+
* instead of removing it (D-78).
|
|
108
|
+
*
|
|
109
|
+
* **Each write audits itself**, in the same unit of work, exactly as
|
|
110
|
+
* `RoleService` and `addresses`' `AddressService` — the two write surfaces
|
|
111
|
+
* `organizations` already reaches across this boundary — have always done. The
|
|
112
|
+
* host's own row is a different fact and stays where it is: "an operator edited
|
|
113
|
+
* this member on the organisation panel" is not "this account's e-mail
|
|
114
|
+
* changed", and the role endpoint has recorded both for as long as it has
|
|
115
|
+
* existed.
|
|
116
|
+
*/
|
|
117
|
+
export declare class CustomerAccountMemberWriteService implements CustomerAccountMemberWritePort {
|
|
118
|
+
#private;
|
|
119
|
+
private readonly emFactory;
|
|
120
|
+
private readonly twoFactorEnrolments;
|
|
121
|
+
private readonly auditLog?;
|
|
122
|
+
constructor(emFactory: () => EntityManager, twoFactorEnrolments: TwoFactorEnrolmentReader, auditLog?: AuditPort | undefined);
|
|
123
|
+
create(input: CustomerAccountCreateInput): Promise<CustomerAccountRecord>;
|
|
124
|
+
updateProfile(customerAccountId: string, patch: CustomerAccountProfilePatch): Promise<CustomerAccountRecord>;
|
|
125
|
+
setSubtreeRollup(customerAccountId: string, enabled: boolean): Promise<CustomerAccountRecord>;
|
|
126
|
+
promoteToOrganizationAdmin(customerAccountId: string): Promise<CustomerAccountRecord>;
|
|
127
|
+
markEmailVerified(customerAccountId: string, verifiedAt: Date): Promise<CustomerAccountRecord>;
|
|
128
|
+
attachToOrganization(customerAccountId: string, organizationId: string): Promise<CustomerAccountRecord>;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* `CustomerAuthService.login` resolves to a `LoginResult` carrying the
|
|
132
|
+
* `CustomerAccount` entity. The port hands back the record instead — the same
|
|
133
|
+
* substitution the read service makes, for the same reason.
|
|
134
|
+
*
|
|
135
|
+
* The service arrives as a **getter** rather than a value so the adapter reads
|
|
136
|
+
* it from the container per call. Constructing a second `CustomerAuthService`
|
|
137
|
+
* here is precisely the divergence this module's conversion removed: the two
|
|
138
|
+
* composition roots each built one, and their MFA argument differed.
|
|
139
|
+
*/
|
|
140
|
+
export declare function createCustomerAuthPort(getService: () => CustomerAuthService, twoFactorEnrolments: TwoFactorEnrolmentReader): CustomerAuthPort;
|
|
141
|
+
/** Same substitution for the member-management surface `organizations` calls. */
|
|
142
|
+
export declare function createCustomerRolePort(getService: () => RoleService, twoFactorEnrolments: TwoFactorEnrolmentReader): CustomerRolePort;
|
|
143
|
+
//# sourceMappingURL=customer-account-ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-account-ports.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-account-ports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { UniqueConstraintViolationException } from '@mikro-orm/core';
|
|
2
|
+
import { ERROR_CODES, normalizeEmailAddress, } from '@endora-commerce/contracts';
|
|
3
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
4
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
5
|
+
import { hashPassword, verifyPassword } from '@endora-commerce/platform/kernel';
|
|
6
|
+
import { CustomerAccount } from '../entities/customer-account.entity.js';
|
|
7
|
+
/**
|
|
8
|
+
* The published face of `customer_accounts` (feature 075, Phase P).
|
|
9
|
+
*
|
|
10
|
+
* Nineteen modules read this module's table today, 51 of the sites on the
|
|
11
|
+
* entity class itself. What they ask for is narrow — an account by id, by ids,
|
|
12
|
+
* by e-mail, by organisation, and the two counting questions the "an
|
|
13
|
+
* organisation keeps at least one admin" rule is spelled with — so that is
|
|
14
|
+
* what the read port is, rather than the table.
|
|
15
|
+
*
|
|
16
|
+
* The mapping below is the point of the exercise and not ceremony:
|
|
17
|
+
* `passwordHash` does not survive it. It is read by exactly one module, and a
|
|
18
|
+
* record that carried it would turn every consumer into a place a credential
|
|
19
|
+
* can leak from.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The published record, with `twoFactorEnabled` taken from `mfa` rather than
|
|
23
|
+
* from this module's `two_factor_confirmed_at` column.
|
|
24
|
+
*
|
|
25
|
+
* The second parameter is required and has no default on purpose. That column's
|
|
26
|
+
* only non-null writer was the superseded TOTP path deleted on 2026-08-25, so
|
|
27
|
+
* the field was a **provably constant `false`** — on the admin customer detail,
|
|
28
|
+
* on both organisation member panels, and on `GET /api/v1/me/customer`, which
|
|
29
|
+
* is the buyer's own account page telling them their account is unprotected. A
|
|
30
|
+
* default would let the next call site reintroduce that silently; making the
|
|
31
|
+
* caller answer means the compiler asks.
|
|
32
|
+
*/
|
|
33
|
+
export function toCustomerAccountRecord(account, twoFactorEnabled) {
|
|
34
|
+
return {
|
|
35
|
+
id: account.id,
|
|
36
|
+
organizationId: account.organizationId ?? null,
|
|
37
|
+
email: account.email,
|
|
38
|
+
firstName: account.firstName,
|
|
39
|
+
lastName: account.lastName,
|
|
40
|
+
role: account.role,
|
|
41
|
+
emailVerifiedAt: account.emailVerifiedAt ?? null,
|
|
42
|
+
twoFactorEnabled,
|
|
43
|
+
lastLoginAt: account.lastLoginAt ?? null,
|
|
44
|
+
createdAt: account.createdAt,
|
|
45
|
+
updatedAt: account.updatedAt,
|
|
46
|
+
customFieldValues: account.customFieldValues ?? {},
|
|
47
|
+
deletedAt: account.deletedAt ?? null,
|
|
48
|
+
customerGroupId: account.customerGroupId ?? null,
|
|
49
|
+
subtreeRollupEnabled: account.subtreeRollupEnabled,
|
|
50
|
+
blockedAt: account.blockedAt ?? null,
|
|
51
|
+
blockReason: account.blockReason ?? null,
|
|
52
|
+
blockSource: account.blockSource ?? null,
|
|
53
|
+
blockedByAdminUserId: account.blockedByAdminUserId ?? null,
|
|
54
|
+
blockedByCustomerAccountId: account.blockedByCustomerAccountId ?? null,
|
|
55
|
+
deletionRequestedByAdminUserId: account.deletionRequestedByAdminUserId ?? null,
|
|
56
|
+
anonymizedAt: account.anonymizedAt ?? null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* One `mfa` read for a whole page of accounts, rather than one per row.
|
|
61
|
+
*
|
|
62
|
+
* Every caller is a list or a single row, so batching here keeps the admin
|
|
63
|
+
* customer list at two queries instead of one per customer, and leaves the
|
|
64
|
+
* single-row callers spelling the same thing.
|
|
65
|
+
*/
|
|
66
|
+
export async function toCustomerAccountRecords(twoFactorEnrolments, accounts) {
|
|
67
|
+
if (accounts.length === 0)
|
|
68
|
+
return [];
|
|
69
|
+
const enrolled = await twoFactorEnrolments(accounts.map((account) => account.id));
|
|
70
|
+
return accounts.map((account) => toCustomerAccountRecord(account, enrolled.has(account.id)));
|
|
71
|
+
}
|
|
72
|
+
/** The single-row spelling of {@link toCustomerAccountRecords}. */
|
|
73
|
+
export async function toOneCustomerAccountRecord(twoFactorEnrolments, account) {
|
|
74
|
+
return (await toCustomerAccountRecords(twoFactorEnrolments, [account]))[0];
|
|
75
|
+
}
|
|
76
|
+
/** `{ deletedAt: null }` when the caller asked for it, nothing otherwise. */
|
|
77
|
+
function activeFilter(options) {
|
|
78
|
+
return options?.activeOnly ? { deletedAt: null } : {};
|
|
79
|
+
}
|
|
80
|
+
export class CustomerAccountReadService {
|
|
81
|
+
emFactory;
|
|
82
|
+
twoFactorEnrolments;
|
|
83
|
+
constructor(emFactory, twoFactorEnrolments) {
|
|
84
|
+
this.emFactory = emFactory;
|
|
85
|
+
this.twoFactorEnrolments = twoFactorEnrolments;
|
|
86
|
+
}
|
|
87
|
+
async findById(id, options) {
|
|
88
|
+
const account = await this.emFactory().findOne(CustomerAccount, {
|
|
89
|
+
id,
|
|
90
|
+
...activeFilter(options),
|
|
91
|
+
});
|
|
92
|
+
return account ? toOneCustomerAccountRecord(this.twoFactorEnrolments, account) : null;
|
|
93
|
+
}
|
|
94
|
+
async findByIds(ids, options) {
|
|
95
|
+
if (ids.length === 0)
|
|
96
|
+
return [];
|
|
97
|
+
const accounts = await this.emFactory().find(CustomerAccount, {
|
|
98
|
+
id: { $in: [...ids] },
|
|
99
|
+
...activeFilter(options),
|
|
100
|
+
});
|
|
101
|
+
return toCustomerAccountRecords(this.twoFactorEnrolments, accounts);
|
|
102
|
+
}
|
|
103
|
+
async findByEmail(email, options) {
|
|
104
|
+
// The fold is here rather than at the four callers: this is the one
|
|
105
|
+
// question "is this address taken" is asked through, and a caller that
|
|
106
|
+
// folded its own way (or forgot to) is what made a registered account
|
|
107
|
+
// unfindable by the address its holder had typed.
|
|
108
|
+
const account = await this.emFactory().findOne(CustomerAccount, {
|
|
109
|
+
email: normalizeEmailAddress(email),
|
|
110
|
+
...activeFilter(options),
|
|
111
|
+
});
|
|
112
|
+
return account ? toOneCustomerAccountRecord(this.twoFactorEnrolments, account) : null;
|
|
113
|
+
}
|
|
114
|
+
async findInOrganization(id, organizationId, options) {
|
|
115
|
+
const account = await this.emFactory().findOne(CustomerAccount, {
|
|
116
|
+
id,
|
|
117
|
+
organizationId,
|
|
118
|
+
...activeFilter(options),
|
|
119
|
+
});
|
|
120
|
+
return account ? toOneCustomerAccountRecord(this.twoFactorEnrolments, account) : null;
|
|
121
|
+
}
|
|
122
|
+
async listByOrganization(organizationId, options) {
|
|
123
|
+
const accounts = await this.emFactory().find(CustomerAccount, { organizationId, ...activeFilter(options) }, { orderBy: { role: 'asc', createdAt: 'asc' } });
|
|
124
|
+
return toCustomerAccountRecords(this.twoFactorEnrolments, accounts);
|
|
125
|
+
}
|
|
126
|
+
async countByOrganizationRole(organizationId, role, options) {
|
|
127
|
+
return this.emFactory().count(CustomerAccount, {
|
|
128
|
+
organizationId,
|
|
129
|
+
role,
|
|
130
|
+
...(options?.activeOnly ? { deletedAt: null } : {}),
|
|
131
|
+
...(options?.notBlocked ? { blockedAt: null } : {}),
|
|
132
|
+
...(options?.excludeCustomerAccountId
|
|
133
|
+
? { id: { $ne: options.excludeCustomerAccountId } }
|
|
134
|
+
: {}),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async searchByEmail(query, limit) {
|
|
138
|
+
const trimmed = query.trim();
|
|
139
|
+
const accounts = await this.emFactory().find(CustomerAccount, trimmed ? { email: { $ilike: `%${trimmed}%` } } : {}, { orderBy: { email: 'asc' }, limit });
|
|
140
|
+
return toCustomerAccountRecords(this.twoFactorEnrolments, accounts);
|
|
141
|
+
}
|
|
142
|
+
async searchIdsByName(query) {
|
|
143
|
+
const trimmed = query.trim();
|
|
144
|
+
if (!trimmed)
|
|
145
|
+
return [];
|
|
146
|
+
const like = `%${trimmed}%`;
|
|
147
|
+
const accounts = await this.emFactory().find(CustomerAccount, {
|
|
148
|
+
$or: [
|
|
149
|
+
{ email: { $ilike: like } },
|
|
150
|
+
{ firstName: { $ilike: like } },
|
|
151
|
+
{ lastName: { $ilike: like } },
|
|
152
|
+
],
|
|
153
|
+
}, { fields: ['id'] });
|
|
154
|
+
return accounts.map((account) => account.id);
|
|
155
|
+
}
|
|
156
|
+
async listAll() {
|
|
157
|
+
const accounts = await this.emFactory().find(CustomerAccount, {}, { orderBy: { email: 'asc' } });
|
|
158
|
+
return toCustomerAccountRecords(this.twoFactorEnrolments, accounts);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Issue #222 — the one credential fact that crosses this boundary, and it
|
|
163
|
+
* crosses as a date rather than as anything derived from the hash.
|
|
164
|
+
*
|
|
165
|
+
* Separate from {@link CustomerAccountReadService} on purpose: that read port
|
|
166
|
+
* is what nineteen modules resolve, and the mapping above deliberately drops
|
|
167
|
+
* `passwordHash`. "Does this account have a password its holder can use" is a
|
|
168
|
+
* question one module asks, so it gets a port one module resolves.
|
|
169
|
+
*
|
|
170
|
+
* An unknown id answers `null`, not a throw: the caller's question is about a
|
|
171
|
+
* credential, and "no account" and "no password on record" want the same
|
|
172
|
+
* refusal from every caller there could be.
|
|
173
|
+
*/
|
|
174
|
+
export class CustomerPasswordStateService {
|
|
175
|
+
emFactory;
|
|
176
|
+
constructor(emFactory) {
|
|
177
|
+
this.emFactory = emFactory;
|
|
178
|
+
}
|
|
179
|
+
async passwordSetAt(customerAccountId) {
|
|
180
|
+
// A full load rather than a projection: a partially-loaded entity in the
|
|
181
|
+
// identity map is a hazard for whatever flushes next in the same unit of
|
|
182
|
+
// work, and only the date leaves this method either way.
|
|
183
|
+
const account = await this.emFactory().findOne(CustomerAccount, { id: customerAccountId });
|
|
184
|
+
return account?.passwordSetAt ?? null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Step-up re-verification, for `mfa` (feature 080, T052).
|
|
189
|
+
*
|
|
190
|
+
* The read is this module's own row and the comparison is the platform
|
|
191
|
+
* hasher's, so both halves are on this side of the seam. The composition roots
|
|
192
|
+
* held them instead: each read `passwordHash` off the entity and called
|
|
193
|
+
* `verifyPassword` itself, which is a credential column and a hash comparison
|
|
194
|
+
* living in a file that owns neither.
|
|
195
|
+
*
|
|
196
|
+
* The hash never leaves this class — that is the difference between this port
|
|
197
|
+
* and a `passwordHash` field on `CustomerAccountRecord`, and it is the whole
|
|
198
|
+
* reason the port exists.
|
|
199
|
+
*/
|
|
200
|
+
export class CustomerPasswordVerificationService {
|
|
201
|
+
emFactory;
|
|
202
|
+
constructor(emFactory) {
|
|
203
|
+
this.emFactory = emFactory;
|
|
204
|
+
}
|
|
205
|
+
async verifyPassword(customerAccountId, password) {
|
|
206
|
+
const account = await this.emFactory().findOne(CustomerAccount, { id: customerAccountId });
|
|
207
|
+
// The bare call is the imported hasher: a class method is not in scope as
|
|
208
|
+
// an identifier, so it shadows nothing.
|
|
209
|
+
return account ? verifyPassword(account.passwordHash, password) : false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The member lifecycle `organizations` runs over this module's table
|
|
214
|
+
* (feature 075, Phase C).
|
|
215
|
+
*
|
|
216
|
+
* It ran it by creating and mutating the `CustomerAccount` entity in its own
|
|
217
|
+
* services and route handlers — seven write sites across five files. They are
|
|
218
|
+
* six methods here, each one unit of work on this table alone, because a write
|
|
219
|
+
* port that took the caller's `EntityManager` would publish the coupling
|
|
220
|
+
* instead of removing it (D-78).
|
|
221
|
+
*
|
|
222
|
+
* **Each write audits itself**, in the same unit of work, exactly as
|
|
223
|
+
* `RoleService` and `addresses`' `AddressService` — the two write surfaces
|
|
224
|
+
* `organizations` already reaches across this boundary — have always done. The
|
|
225
|
+
* host's own row is a different fact and stays where it is: "an operator edited
|
|
226
|
+
* this member on the organisation panel" is not "this account's e-mail
|
|
227
|
+
* changed", and the role endpoint has recorded both for as long as it has
|
|
228
|
+
* existed.
|
|
229
|
+
*/
|
|
230
|
+
export class CustomerAccountMemberWriteService {
|
|
231
|
+
emFactory;
|
|
232
|
+
twoFactorEnrolments;
|
|
233
|
+
auditLog;
|
|
234
|
+
constructor(emFactory, twoFactorEnrolments, auditLog) {
|
|
235
|
+
this.emFactory = emFactory;
|
|
236
|
+
this.twoFactorEnrolments = twoFactorEnrolments;
|
|
237
|
+
this.auditLog = auditLog;
|
|
238
|
+
}
|
|
239
|
+
async create(input) {
|
|
240
|
+
const em = this.emFactory();
|
|
241
|
+
const email = normalizeEmailAddress(input.email);
|
|
242
|
+
const existing = await em.findOne(CustomerAccount, { email });
|
|
243
|
+
if (existing) {
|
|
244
|
+
throw new HttpError(409, ERROR_CODES.EMAIL_ALREADY_REGISTERED, 'An account with this email already exists.');
|
|
245
|
+
}
|
|
246
|
+
const account = em.create(CustomerAccount, {
|
|
247
|
+
organizationId: input.organizationId,
|
|
248
|
+
email,
|
|
249
|
+
passwordHash: await hashPassword(input.password),
|
|
250
|
+
// Issue #222 — a password somebody supplied, so the account has one on
|
|
251
|
+
// record from its first moment. This is the write both organisation
|
|
252
|
+
// registration (the holder typed it) and the member admin surface (an
|
|
253
|
+
// operator set it and hands it over) go through.
|
|
254
|
+
passwordSetAt: new Date(),
|
|
255
|
+
firstName: input.firstName,
|
|
256
|
+
lastName: input.lastName,
|
|
257
|
+
role: input.role,
|
|
258
|
+
...(input.emailVerified ? { emailVerifiedAt: new Date() } : {}),
|
|
259
|
+
});
|
|
260
|
+
this.#audit(em, 'customer_account.create', account.id, null, {
|
|
261
|
+
organizationId: account.organizationId ?? null,
|
|
262
|
+
email: account.email,
|
|
263
|
+
role: account.role,
|
|
264
|
+
});
|
|
265
|
+
try {
|
|
266
|
+
await em.persistAndFlush(account);
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
// The unique index is the real arbiter; the pre-check above only buys a
|
|
270
|
+
// better message for the common case. Two registrations racing on one
|
|
271
|
+
// address must both answer 409 rather than one answering 500.
|
|
272
|
+
if (err instanceof UniqueConstraintViolationException) {
|
|
273
|
+
throw new HttpError(409, ERROR_CODES.EMAIL_ALREADY_REGISTERED, 'An account with this email already exists.');
|
|
274
|
+
}
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
278
|
+
}
|
|
279
|
+
async updateProfile(customerAccountId, patch) {
|
|
280
|
+
const em = this.emFactory();
|
|
281
|
+
const account = await this.#loadLive(em, customerAccountId);
|
|
282
|
+
const before = {
|
|
283
|
+
email: account.email,
|
|
284
|
+
firstName: account.firstName,
|
|
285
|
+
lastName: account.lastName,
|
|
286
|
+
};
|
|
287
|
+
if (patch.email !== undefined) {
|
|
288
|
+
const nextEmail = normalizeEmailAddress(patch.email);
|
|
289
|
+
if (nextEmail !== account.email) {
|
|
290
|
+
const taken = await em.findOne(CustomerAccount, { email: nextEmail, deletedAt: null });
|
|
291
|
+
if (taken && taken.id !== account.id) {
|
|
292
|
+
throw new HttpError(409, ERROR_CODES.EMAIL_ALREADY_REGISTERED, 'That email is already used by another account.');
|
|
293
|
+
}
|
|
294
|
+
account.email = nextEmail;
|
|
295
|
+
account.emailVerifiedAt = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (patch.firstName !== undefined)
|
|
299
|
+
account.firstName = patch.firstName;
|
|
300
|
+
if (patch.lastName !== undefined)
|
|
301
|
+
account.lastName = patch.lastName;
|
|
302
|
+
this.#audit(em, 'customer_account.update_profile', account.id, before, {
|
|
303
|
+
email: account.email,
|
|
304
|
+
firstName: account.firstName,
|
|
305
|
+
lastName: account.lastName,
|
|
306
|
+
});
|
|
307
|
+
await em.flush();
|
|
308
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
309
|
+
}
|
|
310
|
+
async setSubtreeRollup(customerAccountId, enabled) {
|
|
311
|
+
const em = this.emFactory();
|
|
312
|
+
const account = await this.#loadLive(em, customerAccountId);
|
|
313
|
+
const before = { subtreeRollupEnabled: account.subtreeRollupEnabled };
|
|
314
|
+
account.subtreeRollupEnabled = enabled;
|
|
315
|
+
this.#audit(em, 'customer_account.set_subtree_rollup', account.id, before, {
|
|
316
|
+
subtreeRollupEnabled: enabled,
|
|
317
|
+
});
|
|
318
|
+
await em.flush();
|
|
319
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
320
|
+
}
|
|
321
|
+
async promoteToOrganizationAdmin(customerAccountId) {
|
|
322
|
+
const em = this.emFactory();
|
|
323
|
+
const account = await this.#loadLive(em, customerAccountId);
|
|
324
|
+
const before = { role: account.role };
|
|
325
|
+
account.role = 'organization_admin';
|
|
326
|
+
this.#audit(em, 'customer_account.promote_organization_admin', account.id, before, {
|
|
327
|
+
role: account.role,
|
|
328
|
+
});
|
|
329
|
+
await em.flush();
|
|
330
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
331
|
+
}
|
|
332
|
+
async markEmailVerified(customerAccountId, verifiedAt) {
|
|
333
|
+
const em = this.emFactory();
|
|
334
|
+
const account = await em.findOne(CustomerAccount, { id: customerAccountId });
|
|
335
|
+
if (!account) {
|
|
336
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Customer account not found.');
|
|
337
|
+
}
|
|
338
|
+
// Idempotent: a retried verification keeps the first timestamp, and records
|
|
339
|
+
// nothing the second time — an audit row per retry would say a change
|
|
340
|
+
// happened that did not.
|
|
341
|
+
if (!account.emailVerifiedAt) {
|
|
342
|
+
account.emailVerifiedAt = verifiedAt;
|
|
343
|
+
this.#audit(em, 'customer_account.email_verified', account.id, { emailVerifiedAt: null }, {
|
|
344
|
+
emailVerifiedAt: verifiedAt.toISOString(),
|
|
345
|
+
});
|
|
346
|
+
await em.flush();
|
|
347
|
+
}
|
|
348
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
349
|
+
}
|
|
350
|
+
async attachToOrganization(customerAccountId, organizationId) {
|
|
351
|
+
const em = this.emFactory();
|
|
352
|
+
const account = await em.findOne(CustomerAccount, { id: customerAccountId });
|
|
353
|
+
if (!account) {
|
|
354
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Customer account not found.');
|
|
355
|
+
}
|
|
356
|
+
const before = { organizationId: account.organizationId ?? null };
|
|
357
|
+
account.organizationId = organizationId;
|
|
358
|
+
this.#audit(em, 'customer_account.attach_organization', account.id, before, {
|
|
359
|
+
organizationId,
|
|
360
|
+
});
|
|
361
|
+
await em.flush();
|
|
362
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
363
|
+
}
|
|
364
|
+
#audit(em, action, objectId, stateBefore, stateAfter) {
|
|
365
|
+
if (!this.auditLog)
|
|
366
|
+
return;
|
|
367
|
+
recordAuditFromContext(this.auditLog, em, {
|
|
368
|
+
action,
|
|
369
|
+
objectType: 'customer_account',
|
|
370
|
+
objectId,
|
|
371
|
+
stateBefore,
|
|
372
|
+
stateAfter,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
async #loadLive(em, id) {
|
|
376
|
+
const account = await em.findOne(CustomerAccount, { id, deletedAt: null });
|
|
377
|
+
if (!account) {
|
|
378
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Customer account not found.');
|
|
379
|
+
}
|
|
380
|
+
return account;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* `CustomerAuthService.login` resolves to a `LoginResult` carrying the
|
|
385
|
+
* `CustomerAccount` entity. The port hands back the record instead — the same
|
|
386
|
+
* substitution the read service makes, for the same reason.
|
|
387
|
+
*
|
|
388
|
+
* The service arrives as a **getter** rather than a value so the adapter reads
|
|
389
|
+
* it from the container per call. Constructing a second `CustomerAuthService`
|
|
390
|
+
* here is precisely the divergence this module's conversion removed: the two
|
|
391
|
+
* composition roots each built one, and their MFA argument differed.
|
|
392
|
+
*/
|
|
393
|
+
export function createCustomerAuthPort(getService, twoFactorEnrolments) {
|
|
394
|
+
return {
|
|
395
|
+
async login(input) {
|
|
396
|
+
const outcome = await getService().login(input);
|
|
397
|
+
if (outcome.status !== 'authenticated')
|
|
398
|
+
return outcome;
|
|
399
|
+
return {
|
|
400
|
+
status: 'authenticated',
|
|
401
|
+
customerAccount: await toOneCustomerAccountRecord(twoFactorEnrolments, outcome.customerAccount),
|
|
402
|
+
sessionCookieValue: outcome.sessionCookieValue,
|
|
403
|
+
sessionExpiresAt: outcome.sessionExpiresAt,
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
changePassword: (customerAccountId, currentPassword, newPassword) => getService().changePassword(customerAccountId, currentPassword, newPassword),
|
|
407
|
+
logout: (sessionId) => getService().logout(sessionId),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
/** Same substitution for the member-management surface `organizations` calls. */
|
|
411
|
+
export function createCustomerRolePort(getService, twoFactorEnrolments) {
|
|
412
|
+
return {
|
|
413
|
+
async listMembers(organizationId) {
|
|
414
|
+
return toCustomerAccountRecords(twoFactorEnrolments, await getService().listMembers(organizationId));
|
|
415
|
+
},
|
|
416
|
+
async changeRole(organizationId, targetCustomerAccountId, newRole) {
|
|
417
|
+
return toOneCustomerAccountRecord(twoFactorEnrolments, await getService().changeRole(organizationId, targetCustomerAccountId, newRole));
|
|
418
|
+
},
|
|
419
|
+
removeMember: (organizationId, targetCustomerAccountId) => getService().removeMember(organizationId, targetCustomerAccountId),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
//# sourceMappingURL=customer-account-ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-account-ports.js","sourceRoot":"","sources":["../../../src/backend/services/customer-account-ports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type AuthSessionPort, type MfaLoginPort } from '@endora-commerce/contracts';
|
|
3
|
+
import { CustomerAccount } from '../entities/customer-account.entity.js';
|
|
4
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* Customer-side auth flows (T119; two-step login added in feature 042).
|
|
7
|
+
*
|
|
8
|
+
* - login: email + password → first factor. When an `MfaLoginPort` is injected
|
|
9
|
+
* and the account has active 2FA (or 2FA is enforced for the scope), login
|
|
10
|
+
* returns an `mfaRequired` / `mfaSetupRequired` outcome WITHOUT a session;
|
|
11
|
+
* the second step is completed by the MFA module's verify endpoint. When no
|
|
12
|
+
* port is present (or it returns `proceed`), login issues the session exactly
|
|
13
|
+
* as before (password-only fallback, FR-033).
|
|
14
|
+
* - logout: destroy session.
|
|
15
|
+
* - changePassword: argon2 verify of `currentPassword`; reject with
|
|
16
|
+
* 401 CURRENT_PASSWORD_INVALID otherwise; rehash + persist.
|
|
17
|
+
*
|
|
18
|
+
* Feature 075, Phase C — the three things this service needed from `auth` are
|
|
19
|
+
* now named where they belong rather than in `auth`'s directory. Sessions come
|
|
20
|
+
* over {@link AuthSessionPort}, so a session is minted or destroyed through the
|
|
21
|
+
* surface `auth` publishes and never through its `Session` entity. The MFA seam
|
|
22
|
+
* is `auth`'s *shape* implemented by `mfa`, published in `@endora-commerce/contracts` so
|
|
23
|
+
* this service depends on neither module for it. And the password hash is a
|
|
24
|
+
* pure function that moved to `src/kernel/crypto/`: an operator switching a
|
|
25
|
+
* module off must not make "hash this string" answer 503.
|
|
26
|
+
*/
|
|
27
|
+
export interface LoginResult {
|
|
28
|
+
customerAccount: CustomerAccount;
|
|
29
|
+
/** Value to put into the Set-Cookie header. */
|
|
30
|
+
sessionCookieValue: string;
|
|
31
|
+
sessionExpiresAt: Date;
|
|
32
|
+
}
|
|
33
|
+
/** Discriminated outcome of the first login step (feature 042). */
|
|
34
|
+
export type CustomerLoginOutcome = ({
|
|
35
|
+
status: 'authenticated';
|
|
36
|
+
} & LoginResult) | {
|
|
37
|
+
status: 'mfaRequired';
|
|
38
|
+
challengeId: string;
|
|
39
|
+
} | {
|
|
40
|
+
status: 'mfaSetupRequired';
|
|
41
|
+
setupTicket: string;
|
|
42
|
+
};
|
|
43
|
+
export declare class CustomerAuthService {
|
|
44
|
+
private readonly emFactory;
|
|
45
|
+
private readonly sessionService;
|
|
46
|
+
/** Lazily resolved so composition can late-bind the MFA module. */
|
|
47
|
+
private readonly getMfaLoginPort?;
|
|
48
|
+
private readonly auditLog?;
|
|
49
|
+
constructor(emFactory: () => EntityManager, sessionService: AuthSessionPort,
|
|
50
|
+
/** Lazily resolved so composition can late-bind the MFA module. */
|
|
51
|
+
getMfaLoginPort?: (() => MfaLoginPort | undefined) | undefined, auditLog?: AuditPort | undefined);
|
|
52
|
+
login(input: {
|
|
53
|
+
email: string;
|
|
54
|
+
password: string;
|
|
55
|
+
ip?: string;
|
|
56
|
+
userAgent?: string;
|
|
57
|
+
salesChannelId?: string | null;
|
|
58
|
+
}): Promise<CustomerLoginOutcome>;
|
|
59
|
+
changePassword(customerAccountId: string, currentPassword: string, newPassword: string): Promise<void>;
|
|
60
|
+
logout(sessionId: string): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=customer-auth-service.d.ts.map
|