@endora-commerce/mod-admin-users 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/index.d.ts +47 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +44 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/AdminRolesPage.d.ts +3 -0
- package/dist/admin/pages/AdminRolesPage.d.ts.map +1 -0
- package/dist/admin/pages/AdminRolesPage.js +180 -0
- package/dist/admin/pages/AdminRolesPage.js.map +1 -0
- package/dist/admin/pages/AdminUsersPage.d.ts +3 -0
- package/dist/admin/pages/AdminUsersPage.d.ts.map +1 -0
- package/dist/admin/pages/AdminUsersPage.js +151 -0
- package/dist/admin/pages/AdminUsersPage.js.map +1 -0
- package/dist/admin/permission-label.d.ts +26 -0
- package/dist/admin/permission-label.d.ts.map +1 -0
- package/dist/admin/permission-label.js +67 -0
- package/dist/admin/permission-label.js.map +1 -0
- package/dist/backend/cli/create-admin.d.ts +16 -0
- package/dist/backend/cli/create-admin.d.ts.map +1 -0
- package/dist/backend/cli/create-admin.js +101 -0
- package/dist/backend/cli/create-admin.js.map +1 -0
- package/dist/backend/demo/reset.d.ts +15 -0
- package/dist/backend/demo/reset.d.ts.map +1 -0
- package/dist/backend/demo/reset.js +13 -0
- package/dist/backend/demo/reset.js.map +1 -0
- package/dist/backend/demo/rows.d.ts +50 -0
- package/dist/backend/demo/rows.d.ts.map +1 -0
- package/dist/backend/demo/rows.js +73 -0
- package/dist/backend/demo/rows.js.map +1 -0
- package/dist/backend/demo/seed.d.ts +24 -0
- package/dist/backend/demo/seed.d.ts.map +1 -0
- package/dist/backend/demo/seed.js +43 -0
- package/dist/backend/demo/seed.js.map +1 -0
- package/dist/backend/entities/admin-user.entity.d.ts +44 -0
- package/dist/backend/entities/admin-user.entity.d.ts.map +1 -0
- package/dist/backend/entities/admin-user.entity.js +115 -0
- package/dist/backend/entities/admin-user.entity.js.map +1 -0
- package/dist/backend/index.d.ts +77 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +111 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +70 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +50 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +32 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +192 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.impersonation.d.ts +25 -0
- package/dist/backend/routes.impersonation.d.ts.map +1 -0
- package/dist/backend/routes.impersonation.js +108 -0
- package/dist/backend/routes.impersonation.js.map +1 -0
- package/dist/backend/routes.public.d.ts +20 -0
- package/dist/backend/routes.public.d.ts.map +1 -0
- package/dist/backend/routes.public.js +75 -0
- package/dist/backend/routes.public.js.map +1 -0
- package/dist/backend/services/admin-auth-service.d.ts +47 -0
- package/dist/backend/services/admin-auth-service.d.ts.map +1 -0
- package/dist/backend/services/admin-auth-service.js +93 -0
- package/dist/backend/services/admin-auth-service.js.map +1 -0
- package/dist/backend/services/admin-user-ports.d.ts +75 -0
- package/dist/backend/services/admin-user-ports.d.ts.map +1 -0
- package/dist/backend/services/admin-user-ports.js +160 -0
- package/dist/backend/services/admin-user-ports.js.map +1 -0
- package/dist/backend/services/admin-user-service.d.ts +134 -0
- package/dist/backend/services/admin-user-service.d.ts.map +1 -0
- package/dist/backend/services/admin-user-service.js +241 -0
- package/dist/backend/services/admin-user-service.js.map +1 -0
- package/dist/backend/services/impersonation-service.d.ts +65 -0
- package/dist/backend/services/impersonation-service.d.ts.map +1 -0
- package/dist/backend/services/impersonation-service.js +103 -0
- package/dist/backend/services/impersonation-service.js.map +1 -0
- package/dist/backend/services/two-factor-enrolments.d.ts +19 -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 +182 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +208 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260425T053028_admin_users_init.d.ts +11 -0
- package/dist/migrations/20260425T053028_admin_users_init.d.ts.map +1 -0
- package/dist/migrations/20260425T053028_admin_users_init.js +51 -0
- package/dist/migrations/20260425T053028_admin_users_init.js.map +1 -0
- package/dist/migrations/20260819T155150_admin_users_fold_email_case.d.ts +55 -0
- package/dist/migrations/20260819T155150_admin_users_fold_email_case.d.ts.map +1 -0
- package/dist/migrations/20260819T155150_admin_users_fold_email_case.js +106 -0
- package/dist/migrations/20260819T155150_admin_users_fold_email_case.js.map +1 -0
- package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.d.ts +25 -0
- package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.d.ts.map +1 -0
- package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.js +29 -0
- package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.js.map +1 -0
- package/dist/migrations/index.d.ts +29 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +33 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/admin_users.md +53 -0
- package/i18n/en.json +5 -0
- package/i18n/pl.json +5 -0
- package/package.json +100 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type AdminRolePort, type AuthSessionPort } from '@endora-commerce/contracts';
|
|
3
|
+
import { AdminUser } from '../entities/admin-user.entity.js';
|
|
4
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* AdminUserService (T193 / FR-080..FR-083). Backs the admin panel's
|
|
7
|
+
* Users & Roles module. Mutations are gated by `admin_users:manage` at the
|
|
8
|
+
* route layer; this service trusts the caller and focuses on data integrity:
|
|
9
|
+
*
|
|
10
|
+
* - email uniqueness (DB partial unique enforces, mapped to 409)
|
|
11
|
+
* - role existence on assignment
|
|
12
|
+
* - password rehash on create, on self-rotation, and on a peer reset
|
|
13
|
+
* - soft delete via `deletedAt`; status flip is the everyday lever
|
|
14
|
+
*/
|
|
15
|
+
export interface CreateAdminUserInput {
|
|
16
|
+
email: string;
|
|
17
|
+
password: string;
|
|
18
|
+
firstName: string;
|
|
19
|
+
lastName: string;
|
|
20
|
+
adminRoleId?: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface UpdateAdminUserInput {
|
|
23
|
+
firstName?: string;
|
|
24
|
+
lastName?: string;
|
|
25
|
+
adminRoleId?: string | null;
|
|
26
|
+
status?: 'active' | 'inactive';
|
|
27
|
+
/** Optional password rotation. When supplied the value is hashed
|
|
28
|
+
* before being persisted. */
|
|
29
|
+
password?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ListAdminUsersOptions {
|
|
32
|
+
/** Case-insensitive substring on first name, last name, and email. Trimmed. */
|
|
33
|
+
q?: string;
|
|
34
|
+
/** Zero-indexed page (matches the catalog admin convention). */
|
|
35
|
+
page?: number;
|
|
36
|
+
/** Page size. Default 50, max 200. */
|
|
37
|
+
pageSize?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface ListAdminUsersResult {
|
|
40
|
+
items: AdminUser[];
|
|
41
|
+
page: number;
|
|
42
|
+
pageSize: number;
|
|
43
|
+
total: number;
|
|
44
|
+
}
|
|
45
|
+
export declare class AdminUserService {
|
|
46
|
+
#private;
|
|
47
|
+
private readonly emFactory;
|
|
48
|
+
/**
|
|
49
|
+
* `admin_roles`' published role surface (feature 075, Phase C). The role
|
|
50
|
+
* a user is assigned to belongs to that module, so "does this role exist?"
|
|
51
|
+
* is its question and not a second `em.findOne` against its table.
|
|
52
|
+
*/
|
|
53
|
+
private readonly adminRoles;
|
|
54
|
+
/**
|
|
55
|
+
* `auth`'s session surface (feature 075, Phase C). A password write has to
|
|
56
|
+
* be able to withdraw the sessions the old password minted, and those rows
|
|
57
|
+
* belong to `auth`.
|
|
58
|
+
*/
|
|
59
|
+
private readonly sessions;
|
|
60
|
+
private readonly auditLog?;
|
|
61
|
+
constructor(emFactory: () => EntityManager,
|
|
62
|
+
/**
|
|
63
|
+
* `admin_roles`' published role surface (feature 075, Phase C). The role
|
|
64
|
+
* a user is assigned to belongs to that module, so "does this role exist?"
|
|
65
|
+
* is its question and not a second `em.findOne` against its table.
|
|
66
|
+
*/
|
|
67
|
+
adminRoles: AdminRolePort,
|
|
68
|
+
/**
|
|
69
|
+
* `auth`'s session surface (feature 075, Phase C). A password write has to
|
|
70
|
+
* be able to withdraw the sessions the old password minted, and those rows
|
|
71
|
+
* belong to `auth`.
|
|
72
|
+
*/
|
|
73
|
+
sessions: AuthSessionPort, auditLog?: AuditPort | undefined);
|
|
74
|
+
/**
|
|
75
|
+
* List non-deleted admin users with optional substring search and offset
|
|
76
|
+
* pagination. When `q` is provided, results are ordered by a relevance
|
|
77
|
+
* CASE: first-name prefix > first-name contains > last-name prefix >
|
|
78
|
+
* last-name contains > email prefix > email contains, then by email.
|
|
79
|
+
* When `q` is empty, results are ordered by email ASC (legacy default).
|
|
80
|
+
*
|
|
81
|
+
* Note: search is case-insensitive but NOT diacritic-insensitive on the
|
|
82
|
+
* server side — Postgres `unaccent` is not enabled in this codebase. The
|
|
83
|
+
* admin Combobox handles diacritic folding client-side when `manualFilter`
|
|
84
|
+
* is off; the server falls back to plain `LOWER(...) LIKE`. If a future
|
|
85
|
+
* picker (customers, products) needs diacritic-insensitive server search,
|
|
86
|
+
* add `unaccent` via a migration.
|
|
87
|
+
*/
|
|
88
|
+
list(options?: ListAdminUsersOptions): Promise<ListAdminUsersResult>;
|
|
89
|
+
getById(id: string): Promise<AdminUser>;
|
|
90
|
+
/**
|
|
91
|
+
* Batch lookup for display enrichment (e.g. the audit-log actor column):
|
|
92
|
+
* returns the found admin users for the given ids, tolerating unknown or
|
|
93
|
+
* soft-deleted ids (they are simply omitted). Order is not guaranteed.
|
|
94
|
+
*/
|
|
95
|
+
listByIds(ids: string[]): Promise<AdminUser[]>;
|
|
96
|
+
create(input: CreateAdminUserInput): Promise<AdminUser>;
|
|
97
|
+
update(id: string, input: UpdateAdminUserInput): Promise<AdminUser>;
|
|
98
|
+
/**
|
|
99
|
+
* Set an admin user's password on someone else's authority — issue #252.
|
|
100
|
+
*
|
|
101
|
+
* This is the module's password-write seam, and it is written to be the only
|
|
102
|
+
* one: the e-mail-keyed self-service reset that follows the packaging
|
|
103
|
+
* programme differs in **who is authorised** (a token instead of a peer's
|
|
104
|
+
* `admin_users:manage`), not in what the write does. It calls this.
|
|
105
|
+
*
|
|
106
|
+
* Two things happen, in this order:
|
|
107
|
+
*
|
|
108
|
+
* 1. every session the target holds is revoked, and
|
|
109
|
+
* 2. the new hash is persisted with an `admin_user.change_password` audit
|
|
110
|
+
* row that records the target and the route taken — never the password
|
|
111
|
+
* and never its hash.
|
|
112
|
+
*
|
|
113
|
+
* The revocation runs **before** the flush on purpose. It reaches another
|
|
114
|
+
* module, so it can refuse; refusing first means the reset either takes
|
|
115
|
+
* effect whole or not at all, where a flush-then-revoke order could leave a
|
|
116
|
+
* changed password with the old password's sessions still answering. A
|
|
117
|
+
* revocation that succeeded over a flush that then failed only signs the
|
|
118
|
+
* target out, which their existing password undoes.
|
|
119
|
+
*
|
|
120
|
+
* No `catch` around the port call: an operator told "reset" while the old
|
|
121
|
+
* sessions kept working would be told something false.
|
|
122
|
+
*/
|
|
123
|
+
resetPassword(id: string, newPassword: string): Promise<AdminUser>;
|
|
124
|
+
softDelete(id: string): Promise<void>;
|
|
125
|
+
/**
|
|
126
|
+
* Set (or revert) an admin user's preferred Admin UI language —
|
|
127
|
+
* feature 019 / FR-002, FR-004, FR-006. Validation against the
|
|
128
|
+
* supported allowlist lives at the route boundary so this method
|
|
129
|
+
* trusts its input. Passing `null` reverts to the platform default
|
|
130
|
+
* ("no preference saved" → resolver treats as English).
|
|
131
|
+
*/
|
|
132
|
+
setPreferredLanguage(id: string, preferredLanguage: string | null): Promise<AdminUser>;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=admin-user-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-user-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/admin-user-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,eAAe,EACrB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAE7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;;;;;;;GASG;AAEH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC/B;kCAC8B;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,gBAAgB;;IAEzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAbT,SAAS,EAAE,MAAM,aAAa;IAC/C;;;;OAIG;IACc,UAAU,EAAE,aAAa;IAC1C;;;;OAIG;IACc,QAAQ,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,SAAS,YAAA;IASvC;;;;;;;;;;;;;OAaG;IACG,IAAI,CAAC,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA6DxE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAO7C;;;;OAIG;IACG,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAO9C,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,SAAS,CAAC;IA4BvD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,SAAS,CAAC;IAkBzE;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACG,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAgBlE,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS3C;;;;;;OAMG;IACG,oBAAoB,CACxB,EAAE,EAAE,MAAM,EACV,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAC/B,OAAO,CAAC,SAAS,CAAC;CAwBtB"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { UniqueConstraintViolationException } from '@mikro-orm/core';
|
|
2
|
+
import { ERROR_CODES, normalizeEmailAddress, } from '@endora-commerce/contracts';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
4
|
+
import { hashPassword } from '@endora-commerce/platform/kernel';
|
|
5
|
+
import { AdminUser } from '../entities/admin-user.entity.js';
|
|
6
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
7
|
+
export class AdminUserService {
|
|
8
|
+
emFactory;
|
|
9
|
+
adminRoles;
|
|
10
|
+
sessions;
|
|
11
|
+
auditLog;
|
|
12
|
+
constructor(emFactory,
|
|
13
|
+
/**
|
|
14
|
+
* `admin_roles`' published role surface (feature 075, Phase C). The role
|
|
15
|
+
* a user is assigned to belongs to that module, so "does this role exist?"
|
|
16
|
+
* is its question and not a second `em.findOne` against its table.
|
|
17
|
+
*/
|
|
18
|
+
adminRoles,
|
|
19
|
+
/**
|
|
20
|
+
* `auth`'s session surface (feature 075, Phase C). A password write has to
|
|
21
|
+
* be able to withdraw the sessions the old password minted, and those rows
|
|
22
|
+
* belong to `auth`.
|
|
23
|
+
*/
|
|
24
|
+
sessions, auditLog) {
|
|
25
|
+
this.emFactory = emFactory;
|
|
26
|
+
this.adminRoles = adminRoles;
|
|
27
|
+
this.sessions = sessions;
|
|
28
|
+
this.auditLog = auditLog;
|
|
29
|
+
}
|
|
30
|
+
#audit(em, action, objectId, stateBefore, stateAfter) {
|
|
31
|
+
if (this.auditLog) {
|
|
32
|
+
recordAuditFromContext(this.auditLog, em, { action, objectType: 'admin_user', objectId, stateBefore, stateAfter });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* List non-deleted admin users with optional substring search and offset
|
|
37
|
+
* pagination. When `q` is provided, results are ordered by a relevance
|
|
38
|
+
* CASE: first-name prefix > first-name contains > last-name prefix >
|
|
39
|
+
* last-name contains > email prefix > email contains, then by email.
|
|
40
|
+
* When `q` is empty, results are ordered by email ASC (legacy default).
|
|
41
|
+
*
|
|
42
|
+
* Note: search is case-insensitive but NOT diacritic-insensitive on the
|
|
43
|
+
* server side — Postgres `unaccent` is not enabled in this codebase. The
|
|
44
|
+
* admin Combobox handles diacritic folding client-side when `manualFilter`
|
|
45
|
+
* is off; the server falls back to plain `LOWER(...) LIKE`. If a future
|
|
46
|
+
* picker (customers, products) needs diacritic-insensitive server search,
|
|
47
|
+
* add `unaccent` via a migration.
|
|
48
|
+
*/
|
|
49
|
+
async list(options = {}) {
|
|
50
|
+
const em = this.emFactory();
|
|
51
|
+
const page = Math.max(0, options.page ?? 0);
|
|
52
|
+
const pageSize = Math.min(Math.max(1, options.pageSize ?? 50), 200);
|
|
53
|
+
const trimmedQ = options.q?.trim() ?? '';
|
|
54
|
+
if (trimmedQ === '') {
|
|
55
|
+
const [items, total] = await em.findAndCount(AdminUser, { deletedAt: null }, { orderBy: { email: 'asc' }, offset: page * pageSize, limit: pageSize });
|
|
56
|
+
return { items, page, pageSize, total };
|
|
57
|
+
}
|
|
58
|
+
const knex = em.getKnex();
|
|
59
|
+
const needle = trimmedQ.toLowerCase();
|
|
60
|
+
const prefix = `${needle}%`;
|
|
61
|
+
const contains = `%${needle}%`;
|
|
62
|
+
const baseQuery = knex('admin_users')
|
|
63
|
+
.whereNull('deleted_at')
|
|
64
|
+
.andWhere((qb) => {
|
|
65
|
+
qb.whereRaw('LOWER("first_name") LIKE ?', [contains])
|
|
66
|
+
.orWhereRaw('LOWER("last_name") LIKE ?', [contains])
|
|
67
|
+
.orWhereRaw('LOWER("email") LIKE ?', [contains]);
|
|
68
|
+
});
|
|
69
|
+
const totalRow = (await baseQuery
|
|
70
|
+
.clone()
|
|
71
|
+
.count('* as count')
|
|
72
|
+
.first());
|
|
73
|
+
const total = Number(totalRow?.count ?? 0);
|
|
74
|
+
const idRows = (await baseQuery
|
|
75
|
+
.clone()
|
|
76
|
+
.select('id')
|
|
77
|
+
.orderByRaw(`CASE
|
|
78
|
+
WHEN LOWER("first_name") LIKE ? THEN 1
|
|
79
|
+
WHEN LOWER("first_name") LIKE ? THEN 2
|
|
80
|
+
WHEN LOWER("last_name") LIKE ? THEN 3
|
|
81
|
+
WHEN LOWER("last_name") LIKE ? THEN 4
|
|
82
|
+
WHEN LOWER("email") LIKE ? THEN 5
|
|
83
|
+
WHEN LOWER("email") LIKE ? THEN 6
|
|
84
|
+
ELSE 7
|
|
85
|
+
END ASC, "email" ASC`, [prefix, contains, prefix, contains, prefix, contains])
|
|
86
|
+
.offset(page * pageSize)
|
|
87
|
+
.limit(pageSize));
|
|
88
|
+
const ids = idRows.map((r) => r.id);
|
|
89
|
+
if (ids.length === 0)
|
|
90
|
+
return { items: [], page, pageSize, total };
|
|
91
|
+
const found = await em.find(AdminUser, { id: { $in: ids } });
|
|
92
|
+
// Preserve the SQL ordering — `find` with `$in` returns arbitrary order.
|
|
93
|
+
const byId = new Map(found.map((u) => [u.id, u]));
|
|
94
|
+
const items = ids.map((id) => byId.get(id)).filter((u) => Boolean(u));
|
|
95
|
+
return { items, page, pageSize, total };
|
|
96
|
+
}
|
|
97
|
+
async getById(id) {
|
|
98
|
+
const em = this.emFactory();
|
|
99
|
+
const row = await em.findOne(AdminUser, { id, deletedAt: null });
|
|
100
|
+
if (!row)
|
|
101
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Admin user not found.');
|
|
102
|
+
return row;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Batch lookup for display enrichment (e.g. the audit-log actor column):
|
|
106
|
+
* returns the found admin users for the given ids, tolerating unknown or
|
|
107
|
+
* soft-deleted ids (they are simply omitted). Order is not guaranteed.
|
|
108
|
+
*/
|
|
109
|
+
async listByIds(ids) {
|
|
110
|
+
const unique = [...new Set(ids.filter(Boolean))];
|
|
111
|
+
if (unique.length === 0)
|
|
112
|
+
return [];
|
|
113
|
+
const em = this.emFactory();
|
|
114
|
+
return em.find(AdminUser, { id: { $in: unique } });
|
|
115
|
+
}
|
|
116
|
+
async create(input) {
|
|
117
|
+
const em = this.emFactory();
|
|
118
|
+
if (input.adminRoleId)
|
|
119
|
+
await this.#assertRoleExists(input.adminRoleId);
|
|
120
|
+
const passwordHash = await hashPassword(input.password);
|
|
121
|
+
const user = em.create(AdminUser, {
|
|
122
|
+
email: normalizeEmailAddress(input.email),
|
|
123
|
+
passwordHash,
|
|
124
|
+
firstName: input.firstName,
|
|
125
|
+
lastName: input.lastName,
|
|
126
|
+
...(input.adminRoleId ? { adminRoleId: input.adminRoleId } : {}),
|
|
127
|
+
status: 'active',
|
|
128
|
+
});
|
|
129
|
+
this.#audit(em, 'admin_user.create', user.id, null, { email: user.email });
|
|
130
|
+
try {
|
|
131
|
+
await em.persistAndFlush(user);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (err instanceof UniqueConstraintViolationException) {
|
|
135
|
+
throw new HttpError(409, ERROR_CODES.EMAIL_ALREADY_REGISTERED, 'An admin user with that email already exists.');
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
return user;
|
|
140
|
+
}
|
|
141
|
+
async update(id, input) {
|
|
142
|
+
const em = this.emFactory();
|
|
143
|
+
const user = await this.#getByIdOn(em, id);
|
|
144
|
+
if (input.adminRoleId !== undefined) {
|
|
145
|
+
if (input.adminRoleId !== null)
|
|
146
|
+
await this.#assertRoleExists(input.adminRoleId);
|
|
147
|
+
user.adminRoleId = input.adminRoleId;
|
|
148
|
+
}
|
|
149
|
+
if (input.firstName !== undefined)
|
|
150
|
+
user.firstName = input.firstName;
|
|
151
|
+
if (input.lastName !== undefined)
|
|
152
|
+
user.lastName = input.lastName;
|
|
153
|
+
if (input.status !== undefined)
|
|
154
|
+
user.status = input.status;
|
|
155
|
+
if (input.password !== undefined) {
|
|
156
|
+
user.passwordHash = await hashPassword(input.password);
|
|
157
|
+
}
|
|
158
|
+
this.#audit(em, 'admin_user.update', user.id, null, { email: user.email, status: user.status });
|
|
159
|
+
await em.flush();
|
|
160
|
+
return user;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Set an admin user's password on someone else's authority — issue #252.
|
|
164
|
+
*
|
|
165
|
+
* This is the module's password-write seam, and it is written to be the only
|
|
166
|
+
* one: the e-mail-keyed self-service reset that follows the packaging
|
|
167
|
+
* programme differs in **who is authorised** (a token instead of a peer's
|
|
168
|
+
* `admin_users:manage`), not in what the write does. It calls this.
|
|
169
|
+
*
|
|
170
|
+
* Two things happen, in this order:
|
|
171
|
+
*
|
|
172
|
+
* 1. every session the target holds is revoked, and
|
|
173
|
+
* 2. the new hash is persisted with an `admin_user.change_password` audit
|
|
174
|
+
* row that records the target and the route taken — never the password
|
|
175
|
+
* and never its hash.
|
|
176
|
+
*
|
|
177
|
+
* The revocation runs **before** the flush on purpose. It reaches another
|
|
178
|
+
* module, so it can refuse; refusing first means the reset either takes
|
|
179
|
+
* effect whole or not at all, where a flush-then-revoke order could leave a
|
|
180
|
+
* changed password with the old password's sessions still answering. A
|
|
181
|
+
* revocation that succeeded over a flush that then failed only signs the
|
|
182
|
+
* target out, which their existing password undoes.
|
|
183
|
+
*
|
|
184
|
+
* No `catch` around the port call: an operator told "reset" while the old
|
|
185
|
+
* sessions kept working would be told something false.
|
|
186
|
+
*/
|
|
187
|
+
async resetPassword(id, newPassword) {
|
|
188
|
+
const em = this.emFactory();
|
|
189
|
+
const user = await this.#getByIdOn(em, id);
|
|
190
|
+
// Hashed before the revocation so the window between "signed out" and
|
|
191
|
+
// "new password live" is not an argon2 pass wide.
|
|
192
|
+
const passwordHash = await hashPassword(newPassword);
|
|
193
|
+
await this.sessions.destroyAllForAdmin(user.id);
|
|
194
|
+
user.passwordHash = passwordHash;
|
|
195
|
+
this.#audit(em, 'admin_user.change_password', user.id, null, {
|
|
196
|
+
email: user.email,
|
|
197
|
+
via: 'peer_reset',
|
|
198
|
+
});
|
|
199
|
+
await em.flush();
|
|
200
|
+
return user;
|
|
201
|
+
}
|
|
202
|
+
async softDelete(id) {
|
|
203
|
+
const em = this.emFactory();
|
|
204
|
+
const user = await this.#getByIdOn(em, id);
|
|
205
|
+
user.deletedAt = new Date();
|
|
206
|
+
user.status = 'inactive';
|
|
207
|
+
this.#audit(em, 'admin_user.delete', user.id, { email: user.email }, null);
|
|
208
|
+
await em.flush();
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Set (or revert) an admin user's preferred Admin UI language —
|
|
212
|
+
* feature 019 / FR-002, FR-004, FR-006. Validation against the
|
|
213
|
+
* supported allowlist lives at the route boundary so this method
|
|
214
|
+
* trusts its input. Passing `null` reverts to the platform default
|
|
215
|
+
* ("no preference saved" → resolver treats as English).
|
|
216
|
+
*/
|
|
217
|
+
async setPreferredLanguage(id, preferredLanguage) {
|
|
218
|
+
// command-coverage-ignore: per-admin Admin-UI language preference — personal
|
|
219
|
+
// UI setting, not an audited domain-state mutation.
|
|
220
|
+
const em = this.emFactory();
|
|
221
|
+
const user = await this.#getByIdOn(em, id);
|
|
222
|
+
user.preferredLanguage = preferredLanguage;
|
|
223
|
+
await em.flush();
|
|
224
|
+
return user;
|
|
225
|
+
}
|
|
226
|
+
async #getByIdOn(em, id) {
|
|
227
|
+
const row = await em.findOne(AdminUser, { id, deletedAt: null });
|
|
228
|
+
if (!row)
|
|
229
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Admin user not found.');
|
|
230
|
+
return row;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* `getById` raises the same 404 `Admin role not found.` this method used to
|
|
234
|
+
* raise itself, so no `catch` is wanted and none is written: an absent
|
|
235
|
+
* `admin_roles` must refuse the assignment, not let it through unvalidated.
|
|
236
|
+
*/
|
|
237
|
+
async #assertRoleExists(id) {
|
|
238
|
+
await this.adminRoles.getById(id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=admin-user-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-user-service.js","sourceRoot":"","sources":["../../../src/backend/services/admin-user-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type AuthSessionPort, type CustomerAccountReadPort, type CustomerAccountRecord } from '@endora-commerce/contracts';
|
|
3
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
4
|
+
/**
|
|
5
|
+
* ImpersonationService (T189). Implements the switch-user pattern:
|
|
6
|
+
* - start(adminSessionId, customerAccountId, reason?) — destroys (no:
|
|
7
|
+
* DOES NOT destroy) the admin session, mints a new "impersonation"
|
|
8
|
+
* session whose customer scope is the target customer AND whose
|
|
9
|
+
* impersonatorAdminUserId is the original admin. The original admin
|
|
10
|
+
* session id is returned as a "shadow" cookie so end can restore it.
|
|
11
|
+
* - end(impersonationSessionId, adminShadowSessionId) — destroys the
|
|
12
|
+
* impersonation session, returns the (still alive) admin session id.
|
|
13
|
+
*
|
|
14
|
+
* AuditPort.record is called BEFORE the cookie is minted (T179
|
|
15
|
+
* "audit-before-cookie") so a partial failure can never leave an
|
|
16
|
+
* impersonation cookie without an audit row.
|
|
17
|
+
*/
|
|
18
|
+
export interface ImpersonationStartResult {
|
|
19
|
+
impersonationSessionId: string;
|
|
20
|
+
impersonationCookieValue: string;
|
|
21
|
+
impersonationExpiresAt: Date;
|
|
22
|
+
adminShadowSessionCookieValue: string;
|
|
23
|
+
impersonatedCustomerAccount: CustomerAccountRecord;
|
|
24
|
+
}
|
|
25
|
+
export interface ImpersonationEndResult {
|
|
26
|
+
adminSessionCookieValue: string;
|
|
27
|
+
adminSessionExpiresAt: Date;
|
|
28
|
+
}
|
|
29
|
+
export declare class ImpersonationService {
|
|
30
|
+
private readonly emFactory;
|
|
31
|
+
/** `auth`'s published session surface (feature 075, Phase C). */
|
|
32
|
+
private readonly sessionPort;
|
|
33
|
+
/** `customer_accounts`' published read model — the impersonation target. */
|
|
34
|
+
private readonly customerAccounts;
|
|
35
|
+
private readonly auditLog;
|
|
36
|
+
constructor(emFactory: () => EntityManager,
|
|
37
|
+
/** `auth`'s published session surface (feature 075, Phase C). */
|
|
38
|
+
sessionPort: AuthSessionPort,
|
|
39
|
+
/** `customer_accounts`' published read model — the impersonation target. */
|
|
40
|
+
customerAccounts: CustomerAccountReadPort, auditLog: AuditPort);
|
|
41
|
+
start(input: {
|
|
42
|
+
adminUserId: string;
|
|
43
|
+
/** Raw admin session cookie value — must be preserved as the shadow. */
|
|
44
|
+
adminSessionCookieValue: string;
|
|
45
|
+
customerAccountId: string;
|
|
46
|
+
/**
|
|
47
|
+
* Feature 040 — optional: standalone (org-less) customers have no
|
|
48
|
+
* Organization. When provided it scopes the target lookup; when omitted
|
|
49
|
+
* the target is found by id alone.
|
|
50
|
+
*/
|
|
51
|
+
organizationId?: string | null;
|
|
52
|
+
reason?: string;
|
|
53
|
+
ip?: string;
|
|
54
|
+
userAgent?: string;
|
|
55
|
+
requestId?: string;
|
|
56
|
+
}): Promise<ImpersonationStartResult>;
|
|
57
|
+
end(input: {
|
|
58
|
+
impersonationSessionCookieValue: string;
|
|
59
|
+
adminShadowCookieValue: string;
|
|
60
|
+
ip?: string;
|
|
61
|
+
userAgent?: string;
|
|
62
|
+
requestId?: string;
|
|
63
|
+
}): Promise<ImpersonationEndResult>;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=impersonation-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"impersonation-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/impersonation-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC3B,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,wBAAwB;IACvC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,wBAAwB,EAAE,MAAM,CAAC;IACjC,sBAAsB,EAAE,IAAI,CAAC;IAC7B,6BAA6B,EAAE,MAAM,CAAC;IACtC,2BAA2B,EAAE,qBAAqB,CAAC;CACpD;AAED,MAAM,WAAW,sBAAsB;IACrC,uBAAuB,EAAE,MAAM,CAAC;IAChC,qBAAqB,EAAE,IAAI,CAAC;CAC7B;AAED,qBAAa,oBAAoB;IAE7B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBALR,SAAS,EAAE,MAAM,aAAa;IAC/C,iEAAiE;IAChD,WAAW,EAAE,eAAe;IAC7C,4EAA4E;IAC3D,gBAAgB,EAAE,uBAAuB,EACzC,QAAQ,EAAE,SAAS;IAGhC,KAAK,CAAC,KAAK,EAAE;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,wEAAwE;QACxE,uBAAuB,EAAE,MAAM,CAAC;QAChC,iBAAiB,EAAE,MAAM,CAAC;QAC1B;;;;WAIG;QACH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,wBAAwB,CAAC;IAsD/B,GAAG,CAAC,KAAK,EAAE;QACf,+BAA+B,EAAE,MAAM,CAAC;QACxC,sBAAsB,EAAE,MAAM,CAAC;QAC/B,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,sBAAsB,CAAC;CAmDpC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ERROR_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { AdminUser } from '../entities/admin-user.entity.js';
|
|
4
|
+
export class ImpersonationService {
|
|
5
|
+
emFactory;
|
|
6
|
+
sessionPort;
|
|
7
|
+
customerAccounts;
|
|
8
|
+
auditLog;
|
|
9
|
+
constructor(emFactory,
|
|
10
|
+
/** `auth`'s published session surface (feature 075, Phase C). */
|
|
11
|
+
sessionPort,
|
|
12
|
+
/** `customer_accounts`' published read model — the impersonation target. */
|
|
13
|
+
customerAccounts, auditLog) {
|
|
14
|
+
this.emFactory = emFactory;
|
|
15
|
+
this.sessionPort = sessionPort;
|
|
16
|
+
this.customerAccounts = customerAccounts;
|
|
17
|
+
this.auditLog = auditLog;
|
|
18
|
+
}
|
|
19
|
+
async start(input) {
|
|
20
|
+
const em = this.emFactory();
|
|
21
|
+
const admin = await em.findOne(AdminUser, { id: input.adminUserId, deletedAt: null });
|
|
22
|
+
if (!admin) {
|
|
23
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Admin session required.');
|
|
24
|
+
}
|
|
25
|
+
// `findInOrganization` is the two-argument form of the same lookup: the
|
|
26
|
+
// membership check and the identity read in one query, which is what stops
|
|
27
|
+
// the check being forgotten. `activeOnly` carries the `deletedAt: null`
|
|
28
|
+
// half of the read this replaced.
|
|
29
|
+
const target = input.organizationId != null
|
|
30
|
+
? await this.customerAccounts.findInOrganization(input.customerAccountId, input.organizationId, { activeOnly: true })
|
|
31
|
+
: await this.customerAccounts.findById(input.customerAccountId, { activeOnly: true });
|
|
32
|
+
if (!target) {
|
|
33
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Target customer not found.');
|
|
34
|
+
}
|
|
35
|
+
// Audit FIRST — partial failure leaves no dangling cookie.
|
|
36
|
+
await this.auditLog.record({
|
|
37
|
+
actorAdminUserId: admin.id,
|
|
38
|
+
impersonatedCustomerAccountId: target.id,
|
|
39
|
+
action: 'impersonation.start',
|
|
40
|
+
objectType: 'customer_account',
|
|
41
|
+
objectId: target.id,
|
|
42
|
+
stateBefore: null,
|
|
43
|
+
stateAfter: { reason: input.reason ?? null, organizationId: input.organizationId ?? null },
|
|
44
|
+
...(input.ip !== undefined ? { ipAddress: input.ip } : {}),
|
|
45
|
+
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
|
|
46
|
+
...(input.requestId !== undefined ? { requestId: input.requestId } : {}),
|
|
47
|
+
});
|
|
48
|
+
const session = await this.sessionPort.createSession({
|
|
49
|
+
kind: 'impersonation',
|
|
50
|
+
customerAccountId: target.id,
|
|
51
|
+
impersonatorAdminUserId: admin.id,
|
|
52
|
+
...(input.ip !== undefined ? { ipAddress: input.ip } : {}),
|
|
53
|
+
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
impersonationSessionId: session.session.id,
|
|
57
|
+
impersonationCookieValue: session.cookieValue,
|
|
58
|
+
impersonationExpiresAt: session.expiresAt,
|
|
59
|
+
adminShadowSessionCookieValue: input.adminSessionCookieValue,
|
|
60
|
+
impersonatedCustomerAccount: target,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async end(input) {
|
|
64
|
+
// Resolve current impersonation session to get the admin id for the audit
|
|
65
|
+
// entry; then destroy it, then audit, then return the shadow as the new
|
|
66
|
+
// session cookie.
|
|
67
|
+
const resolved = await this.sessionPort.loadSession(input.impersonationSessionCookieValue);
|
|
68
|
+
if (!resolved ||
|
|
69
|
+
resolved.kind !== 'impersonation' ||
|
|
70
|
+
!resolved.session.impersonatorAdminUserId ||
|
|
71
|
+
!resolved.session.customerAccountId) {
|
|
72
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'No active impersonation session.');
|
|
73
|
+
}
|
|
74
|
+
const adminId = resolved.session.impersonatorAdminUserId;
|
|
75
|
+
const customerId = resolved.session.customerAccountId;
|
|
76
|
+
// Destroy impersonation session — single use.
|
|
77
|
+
await this.sessionPort.destroySession(resolved.session.id);
|
|
78
|
+
await this.auditLog.record({
|
|
79
|
+
actorAdminUserId: adminId,
|
|
80
|
+
impersonatedCustomerAccountId: customerId,
|
|
81
|
+
action: 'impersonation.end',
|
|
82
|
+
objectType: 'customer_account',
|
|
83
|
+
objectId: customerId,
|
|
84
|
+
stateBefore: null,
|
|
85
|
+
stateAfter: null,
|
|
86
|
+
...(input.ip !== undefined ? { ipAddress: input.ip } : {}),
|
|
87
|
+
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
|
|
88
|
+
...(input.requestId !== undefined ? { requestId: input.requestId } : {}),
|
|
89
|
+
});
|
|
90
|
+
// The shadow cookie is the unchanged original admin session — verify it
|
|
91
|
+
// still resolves before handing it back. If somehow the admin session
|
|
92
|
+
// expired during impersonation, return 401 so the client must re-login.
|
|
93
|
+
const shadow = await this.sessionPort.loadSession(input.adminShadowCookieValue);
|
|
94
|
+
if (!shadow || shadow.kind !== 'admin') {
|
|
95
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Original admin session is no longer valid; please log in again.');
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
adminSessionCookieValue: input.adminShadowCookieValue,
|
|
99
|
+
adminSessionExpiresAt: shadow.session.expiresAt,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=impersonation-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"impersonation-service.js","sourceRoot":"","sources":["../../../src/backend/services/impersonation-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which admin users hold a second factor — the module's own view of a fact
|
|
3
|
+
* `mfa` owns.
|
|
4
|
+
*
|
|
5
|
+
* A function type rather than an interface, and declared here rather than
|
|
6
|
+
* shared with `customer_accounts`, because there is nothing to share: the
|
|
7
|
+
* published shape is `MfaEnrolmentStatePort` in `@endora-commerce/contracts`,
|
|
8
|
+
* and this is only the shape in which *this* module's serializers want the
|
|
9
|
+
* answer — the `subjectType` argument is already decided, and a `Set` is what a
|
|
10
|
+
* list surface asks per row.
|
|
11
|
+
*
|
|
12
|
+
* `backend.ts` builds the one implementation: it decides `mfa`'s presence and
|
|
13
|
+
* then resolves the port, so an absent owner produces an empty set rather than
|
|
14
|
+
* a `ModuleDisabledError` nobody could answer on an admin list. That is a
|
|
15
|
+
* declared degrade — see the `degrades-without` entry for `mfaEnrolmentStatePort`
|
|
16
|
+
* in this module's manifest — and not a caught gate.
|
|
17
|
+
*/
|
|
18
|
+
export type TwoFactorEnrolmentReader = (adminUserIds: readonly string[]) => Promise<ReadonlySet<string>>;
|
|
19
|
+
//# sourceMappingURL=two-factor-enrolments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"two-factor-enrolments.d.ts","sourceRoot":"","sources":["../../../src/backend/services/two-factor-enrolments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,wBAAwB,GAAG,CACrC,YAAY,EAAE,SAAS,MAAM,EAAE,KAC5B,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"two-factor-enrolments.js","sourceRoot":"","sources":["../../../src/backend/services/two-factor-enrolments.ts"],"names":[],"mappings":""}
|