@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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/admin/index.d.ts +47 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +44 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/pages/AdminRolesPage.d.ts +3 -0
  8. package/dist/admin/pages/AdminRolesPage.d.ts.map +1 -0
  9. package/dist/admin/pages/AdminRolesPage.js +180 -0
  10. package/dist/admin/pages/AdminRolesPage.js.map +1 -0
  11. package/dist/admin/pages/AdminUsersPage.d.ts +3 -0
  12. package/dist/admin/pages/AdminUsersPage.d.ts.map +1 -0
  13. package/dist/admin/pages/AdminUsersPage.js +151 -0
  14. package/dist/admin/pages/AdminUsersPage.js.map +1 -0
  15. package/dist/admin/permission-label.d.ts +26 -0
  16. package/dist/admin/permission-label.d.ts.map +1 -0
  17. package/dist/admin/permission-label.js +67 -0
  18. package/dist/admin/permission-label.js.map +1 -0
  19. package/dist/backend/cli/create-admin.d.ts +16 -0
  20. package/dist/backend/cli/create-admin.d.ts.map +1 -0
  21. package/dist/backend/cli/create-admin.js +101 -0
  22. package/dist/backend/cli/create-admin.js.map +1 -0
  23. package/dist/backend/demo/reset.d.ts +15 -0
  24. package/dist/backend/demo/reset.d.ts.map +1 -0
  25. package/dist/backend/demo/reset.js +13 -0
  26. package/dist/backend/demo/reset.js.map +1 -0
  27. package/dist/backend/demo/rows.d.ts +50 -0
  28. package/dist/backend/demo/rows.d.ts.map +1 -0
  29. package/dist/backend/demo/rows.js +73 -0
  30. package/dist/backend/demo/rows.js.map +1 -0
  31. package/dist/backend/demo/seed.d.ts +24 -0
  32. package/dist/backend/demo/seed.d.ts.map +1 -0
  33. package/dist/backend/demo/seed.js +43 -0
  34. package/dist/backend/demo/seed.js.map +1 -0
  35. package/dist/backend/entities/admin-user.entity.d.ts +44 -0
  36. package/dist/backend/entities/admin-user.entity.d.ts.map +1 -0
  37. package/dist/backend/entities/admin-user.entity.js +115 -0
  38. package/dist/backend/entities/admin-user.entity.js.map +1 -0
  39. package/dist/backend/index.d.ts +77 -0
  40. package/dist/backend/index.d.ts.map +1 -0
  41. package/dist/backend/index.js +111 -0
  42. package/dist/backend/index.js.map +1 -0
  43. package/dist/backend/plugin.d.ts +70 -0
  44. package/dist/backend/plugin.d.ts.map +1 -0
  45. package/dist/backend/plugin.js +50 -0
  46. package/dist/backend/plugin.js.map +1 -0
  47. package/dist/backend/routes.admin.d.ts +32 -0
  48. package/dist/backend/routes.admin.d.ts.map +1 -0
  49. package/dist/backend/routes.admin.js +192 -0
  50. package/dist/backend/routes.admin.js.map +1 -0
  51. package/dist/backend/routes.impersonation.d.ts +25 -0
  52. package/dist/backend/routes.impersonation.d.ts.map +1 -0
  53. package/dist/backend/routes.impersonation.js +108 -0
  54. package/dist/backend/routes.impersonation.js.map +1 -0
  55. package/dist/backend/routes.public.d.ts +20 -0
  56. package/dist/backend/routes.public.d.ts.map +1 -0
  57. package/dist/backend/routes.public.js +75 -0
  58. package/dist/backend/routes.public.js.map +1 -0
  59. package/dist/backend/services/admin-auth-service.d.ts +47 -0
  60. package/dist/backend/services/admin-auth-service.d.ts.map +1 -0
  61. package/dist/backend/services/admin-auth-service.js +93 -0
  62. package/dist/backend/services/admin-auth-service.js.map +1 -0
  63. package/dist/backend/services/admin-user-ports.d.ts +75 -0
  64. package/dist/backend/services/admin-user-ports.d.ts.map +1 -0
  65. package/dist/backend/services/admin-user-ports.js +160 -0
  66. package/dist/backend/services/admin-user-ports.js.map +1 -0
  67. package/dist/backend/services/admin-user-service.d.ts +134 -0
  68. package/dist/backend/services/admin-user-service.d.ts.map +1 -0
  69. package/dist/backend/services/admin-user-service.js +241 -0
  70. package/dist/backend/services/admin-user-service.js.map +1 -0
  71. package/dist/backend/services/impersonation-service.d.ts +65 -0
  72. package/dist/backend/services/impersonation-service.d.ts.map +1 -0
  73. package/dist/backend/services/impersonation-service.js +103 -0
  74. package/dist/backend/services/impersonation-service.js.map +1 -0
  75. package/dist/backend/services/two-factor-enrolments.d.ts +19 -0
  76. package/dist/backend/services/two-factor-enrolments.d.ts.map +1 -0
  77. package/dist/backend/services/two-factor-enrolments.js +2 -0
  78. package/dist/backend/services/two-factor-enrolments.js.map +1 -0
  79. package/dist/manifest.d.ts +182 -0
  80. package/dist/manifest.d.ts.map +1 -0
  81. package/dist/manifest.js +208 -0
  82. package/dist/manifest.js.map +1 -0
  83. package/dist/migrations/20260425T053028_admin_users_init.d.ts +11 -0
  84. package/dist/migrations/20260425T053028_admin_users_init.d.ts.map +1 -0
  85. package/dist/migrations/20260425T053028_admin_users_init.js +51 -0
  86. package/dist/migrations/20260425T053028_admin_users_init.js.map +1 -0
  87. package/dist/migrations/20260819T155150_admin_users_fold_email_case.d.ts +55 -0
  88. package/dist/migrations/20260819T155150_admin_users_fold_email_case.d.ts.map +1 -0
  89. package/dist/migrations/20260819T155150_admin_users_fold_email_case.js +106 -0
  90. package/dist/migrations/20260819T155150_admin_users_fold_email_case.js.map +1 -0
  91. package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.d.ts +25 -0
  92. package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.d.ts.map +1 -0
  93. package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.js +29 -0
  94. package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.js.map +1 -0
  95. package/dist/migrations/index.d.ts +29 -0
  96. package/dist/migrations/index.d.ts.map +1 -0
  97. package/dist/migrations/index.js +33 -0
  98. package/dist/migrations/index.js.map +1 -0
  99. package/docs/admin_users.md +53 -0
  100. package/i18n/en.json +5 -0
  101. package/i18n/pl.json +5 -0
  102. package/package.json +100 -0
  103. package/tailwind.css +14 -0
@@ -0,0 +1,77 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { FastifyRequest } from 'fastify';
3
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
4
+ import type { AdminRolePort, AuthSessionPort, CustomerAccountReadPort, PermissionCataloguePort, PermissionReadPort } from '@endora-commerce/contracts';
5
+ import type { ModuleContext } from '@endora-commerce/platform/kernel';
6
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
7
+ import { adminModule } from './plugin.js';
8
+ import { AdminUser } from './entities/admin-user.entity.js';
9
+ /**
10
+ * `admin_users` — who the admin is, and who is allowed to say so (feature 072,
11
+ * wave 2, T121).
12
+ *
13
+ * Four of its five services came from other modules and are ports now:
14
+ * `sessionService` from `auth`, and `permissionService`,
15
+ * `permissionCatalogueService` and `adminRoleService` from `admin_roles`. The
16
+ * last was optional, with a fallback that built a second `AdminRoleService`
17
+ * inside this module — a shape neither composition ever ran, and one that would
18
+ * have written role changes through a different audit path than the one
19
+ * `/admin-roles` reads.
20
+ *
21
+ * **The MFA port is a `degrades-without` edge this module resolves itself**
22
+ * (D-96). Absent, admin login is password-only, which is a real deployment
23
+ * rather than a broken one; declaring `mfa` in `dependencies` would make the
24
+ * lifecycle refuse to disable MFA while admin login is on, which is backwards —
25
+ * and `mfa` declares *this* module, so the ordinary declaration would close a
26
+ * cycle. The manifest says so in `nonBindingDependencies` instead, and the
27
+ * probe below is what that declaration is held to.
28
+ *
29
+ * It used to be a contribution both roots filled from one getter, which is how
30
+ * a gated port came to be resolved on this module's behalf (composition
31
+ * checklist item 6) — and how the harness came to capture the gate at compose
32
+ * time, so no off-state test could observe `mfa` switched off at all.
33
+ *
34
+ * **`auditActorResolver` stays a root contribution**, against the prediction in
35
+ * `audit_logs/backend.ts` that it would move here. `audit_logs` owns that name
36
+ * and defaults it absent, and the generated composer runs `admin_users` before
37
+ * `audit_logs` — so a registration here would be overwritten by the owner's
38
+ * default a moment later. What the conversion does buy is the gate: the root's
39
+ * adapter now reads the `adminUserService` **port**, whose own gate raises
40
+ * `ModuleDisabledError` when this module is off, so no composition root has to
41
+ * hard-code `isPresent('admin_users')` to decide it.
42
+ */
43
+ /** What `admin_users` resolves from the container, and the names it owns. */
44
+ export interface AdminUsersCradle {
45
+ readonly emFactory: () => EntityManager;
46
+ readonly auditLogService: AuditPort;
47
+ readonly requireAdmin: RequireAdminFactory;
48
+ /** Who the acting admin is — production reads `actor`, the harness `testActor`. */
49
+ readonly adminContextResolver: (req: FastifyRequest) => {
50
+ adminUserId: string;
51
+ };
52
+ readonly authSessionPort: AuthSessionPort;
53
+ readonly permissionService: PermissionReadPort;
54
+ readonly permissionCataloguePort: PermissionCataloguePort;
55
+ readonly adminRolePort: AdminRolePort;
56
+ readonly customerAccountReadPort: CustomerAccountReadPort;
57
+ readonly admin: ReturnType<typeof adminModule>;
58
+ readonly adminUserService: ReturnType<typeof adminModule>['handle']['adminUserService'];
59
+ }
60
+ export declare function registerModule(ctx: ModuleContext): void;
61
+ /**
62
+ * The module's persisted entity classes, on the `./backend` subpath, as one
63
+ * array and **no named class export** (D-168).
64
+ *
65
+ * This is the shape the platform reads when the package is *installed*: the
66
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
67
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
68
+ * which is the third source of `check:module-boundary`'s `table->owner` map and
69
+ * the package pass of `check-entity-tenant-classification`. A missing array is
70
+ * answered with `[]` — zero entities registered, no error anywhere.
71
+ *
72
+ * The order is the one `db/entities-registry.generated.ts` declared before this
73
+ * module became a package, so the registered set is the same list in the same
74
+ * sequence.
75
+ */
76
+ export declare const entities: (typeof AdminUser)[];
77
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAEV,aAAa,EAGb,eAAe,EACf,uBAAuB,EAIvB,uBAAuB,EACvB,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAGtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAS5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,oBAAoB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAChF,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C,QAAQ,CAAC,iBAAiB,EAAE,kBAAkB,CAAC;IAC/C,QAAQ,CAAC,uBAAuB,EAAE,uBAAuB,CAAC;IAC1D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,uBAAuB,EAAE,uBAAuB,CAAC;IAC1D,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IAC/C,QAAQ,CAAC,gBAAgB,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAC,kBAAkB,CAAC,CAAC;CACzF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAmIvD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,QAAQ,sBAEpB,CAAC"}
@@ -0,0 +1,111 @@
1
+ import { lazyPort } from '@endora-commerce/platform/kernel';
2
+ import { effectiveState } from '@endora-commerce/platform/kernel';
3
+ import { adminModule } from './plugin.js';
4
+ import { AdminUser } from './entities/admin-user.entity.js';
5
+ import { AdminUserReadService, createAdminPasswordVerificationPort, createAdminUserPreferencePort, createImpersonationPort, } from './services/admin-user-ports.js';
6
+ export function registerModule(ctx) {
7
+ // D-96 — resolved once, called through on every login. The gate `mfa`'s
8
+ // `providePort` put on this name is transient, so the proxy asks about the
9
+ // module's effective state at each call rather than at composition.
10
+ const mfaLoginPort = lazyPort(ctx, 'mfaLoginPort');
11
+ // The live source of `twoFactorEnabled`, on the same terms as the login port
12
+ // above and for the same reason. This module's own `two_factor_confirmed_at`
13
+ // column has never had a writer, so the field derived from it was a provably
14
+ // constant `false` on `/admin-users`, on `/admin/me` and on the login
15
+ // response the Admin UI seeds its auth context from.
16
+ const mfaEnrolmentStatePort = lazyPort(ctx, 'mfaEnrolmentStatePort');
17
+ /**
18
+ * The degrade is performed by **not resolving**, exactly as `getMfaLoginPort`
19
+ * below does it — presence is decided first and there is deliberately no
20
+ * `catch` anywhere near the port. An admin list is the wrong place for a 503,
21
+ * and `false` is not a substitute for the answer here: with `mfa` absent no
22
+ * sign-in asks for a second factor, so no account is protected by one. The
23
+ * sentence an operator is shown before the flip is this module's
24
+ * `degrades-without` entry for `mfaEnrolmentStatePort`.
25
+ */
26
+ const twoFactorEnrolments = async (adminUserIds) => {
27
+ if (!effectiveState.isPresent('mfa'))
28
+ return new Set();
29
+ return new Set(await mfaEnrolmentStatePort.activeSubjectIds('admin', adminUserIds));
30
+ };
31
+ ctx.di.register({
32
+ admin: ctx
33
+ .asFunction(({ emFactory, auditLogService }) => adminModule({
34
+ emFactory,
35
+ auditLogService,
36
+ // Feature 075, Phase C — every collaborator below is another
37
+ // module's published port, resolved lazily by a string literal so
38
+ // the gate answers per call and nothing captures it.
39
+ authSessionPort: lazyPort(ctx, 'authSessionPort'),
40
+ permissionService: lazyPort(ctx, 'permissionService'),
41
+ permissionCataloguePort: lazyPort(ctx, 'permissionCataloguePort'),
42
+ adminRolePort: lazyPort(ctx, 'adminRolePort'),
43
+ customerAccountReadPort: lazyPort(ctx, 'customerAccountReadPort'),
44
+ requireAdmin: (permission) => async (req, reply) => ctx.cradle().requireAdmin(permission)(req, reply),
45
+ resolveAdminContext: (req) => ctx.cradle().adminContextResolver(req),
46
+ // D-96 — the degrade is performed by **not resolving**. The port's
47
+ // gate throws `ModuleDisabledError` when `mfa` is absent, and there
48
+ // is deliberately no `catch` anywhere near this: a caught gate is a
49
+ // fail-open degrade nobody declared. Deciding presence first is the
50
+ // `auth`/`api_keys` shape, and `AdminAuthService`'s `if (mfaPort)`
51
+ // branch — the password-only fallback of feature 042 FR-033 — is what
52
+ // `undefined` selects.
53
+ getMfaLoginPort: () => (effectiveState.isPresent('mfa') ? mfaLoginPort : undefined),
54
+ twoFactorEnrolments,
55
+ }))
56
+ .singleton(),
57
+ });
58
+ ctx.di.providePort('adminUserService', ctx.asFunction(({ admin }) => admin.handle.adminUserService).singleton());
59
+ // ---------------------------------------------------------------------------
60
+ // Feature 075, Phase P — the published surface.
61
+ //
62
+ // Seven of the eleven inbound sites read the `AdminUser` **entity** to put a
63
+ // name beside an id, and none of them wants a service: `admin_roles`
64
+ // resolving a user's role, `quote_requests` listing the admins a
65
+ // notification fans out to, `catalog` attributing a bulk operation,
66
+ // `organizations` rendering the sales-rep picker. `adminUserReadPort` is
67
+ // that read.
68
+ //
69
+ // The two adapters narrow the module's services to what one consumer each
70
+ // calls — `_i18n` writes a language preference, `customers` starts and ends
71
+ // an impersonation — and neither hands an entity across.
72
+ // ---------------------------------------------------------------------------
73
+ ctx.di.providePort('adminUserReadPort', ctx
74
+ .asFunction(({ emFactory }) => new AdminUserReadService(emFactory, twoFactorEnrolments))
75
+ .singleton());
76
+ // Feature 080 (T052) — step-up re-verification, for `mfa`. Both composition
77
+ // roots used to read `passwordHash` off this module's entity and compare it
78
+ // with the platform hasher themselves; the column and the comparison are
79
+ // this module's, so the boolean is what crosses.
80
+ ctx.di.providePort('adminPasswordVerificationPort', ctx
81
+ .asFunction(({ emFactory }) => createAdminPasswordVerificationPort(emFactory))
82
+ .singleton());
83
+ ctx.di.providePort('adminUserPreferencePort', ctx
84
+ .asFunction(() => createAdminUserPreferencePort(() => ctx.cradle().adminUserService, twoFactorEnrolments))
85
+ .singleton());
86
+ ctx.di.providePort('impersonationPort', ctx
87
+ .asFunction(({ admin }) => createImpersonationPort(() => admin.handle.impersonationService))
88
+ .singleton());
89
+ ctx.routes(async (app) => {
90
+ await ctx.cradle().admin.plugin(app);
91
+ });
92
+ }
93
+ /**
94
+ * The module's persisted entity classes, on the `./backend` subpath, as one
95
+ * array and **no named class export** (D-168).
96
+ *
97
+ * This is the shape the platform reads when the package is *installed*: the
98
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
99
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
100
+ * which is the third source of `check:module-boundary`'s `table->owner` map and
101
+ * the package pass of `check-entity-tenant-classification`. A missing array is
102
+ * answered with `[]` — zero entities registered, no error anywhere.
103
+ *
104
+ * The order is the one `db/entities-registry.generated.ts` declared before this
105
+ * module became a package, so the registered set is the same list in the same
106
+ * sequence.
107
+ */
108
+ export const entities = [
109
+ AdminUser,
110
+ ];
111
+ //# 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,70 @@
1
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ import type { AdminRolePort, AuthSessionPort, CustomerAccountReadPort, MfaLoginPort, PermissionCataloguePort, PermissionReadPort } from '@endora-commerce/contracts';
4
+ import { AdminAuthService } from './services/admin-auth-service.js';
5
+ import { ImpersonationService } from './services/impersonation-service.js';
6
+ import { AdminUserService } from './services/admin-user-service.js';
7
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
8
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
9
+ import type { TwoFactorEnrolmentReader } from './services/two-factor-enrolments.js';
10
+ /**
11
+ * Every collaborator this module does not own is named by its **container
12
+ * name** and typed by the contract its owner publishes (feature 075, Phase C).
13
+ * The field names are the port names `backend.ts` resolves, so the option
14
+ * object, the `lazyPort` literal and the manifest dependency read as one thing.
15
+ */
16
+ export interface AdminModuleOptions {
17
+ emFactory: () => EntityManager;
18
+ /** `auth`'s session surface — mints, loads and destroys the session rows. */
19
+ authSessionPort: AuthSessionPort;
20
+ auditLogService: AuditPort;
21
+ /** `admin_roles` — the effective permission codes of one admin user. */
22
+ permissionService: PermissionReadPort;
23
+ /** `admin_roles` — the codes an operator may actually grant. */
24
+ permissionCataloguePort: PermissionCataloguePort;
25
+ /**
26
+ * Required since feature 072 (T121). It was optional with an in-module
27
+ * fallback that built a second `AdminRoleService`; both compositions passed
28
+ * `admin_roles`' own instance, and the fallback would have written role
29
+ * changes through a different audit path than `/admin-roles` reads.
30
+ */
31
+ adminRolePort: AdminRolePort;
32
+ /** `customer_accounts` — who an impersonation is started against. */
33
+ customerAccountReadPort: CustomerAccountReadPort;
34
+ requireAdmin: RequireAdminFactory;
35
+ /** Resolves the current admin's id from `request.actor` (prod) or
36
+ * `request.testActor` (test harness). Used by `GET /admin/me`. */
37
+ resolveAdminContext: (req: FastifyRequest) => {
38
+ adminUserId: string;
39
+ };
40
+ /** Feature 042 — lazily resolved MFA login port (absent ⇒ password-only). */
41
+ getMfaLoginPort?: () => MfaLoginPort | undefined;
42
+ /**
43
+ * Which admins hold a second factor. Required, not optional: the field it
44
+ * feeds has a value on every response that carries an admin user, and the
45
+ * defect being repaired is exactly what a plausible default produces.
46
+ */
47
+ twoFactorEnrolments: TwoFactorEnrolmentReader;
48
+ }
49
+ export interface AdminModuleHandle {
50
+ adminAuthService: AdminAuthService;
51
+ impersonationService: ImpersonationService;
52
+ permissionService: PermissionReadPort;
53
+ auditLogService: AuditPort;
54
+ /**
55
+ * Exposed so a composition root can contribute `auditActorResolver` to
56
+ * `audit_logs` (feature 072, T084). That module used to be mounted from
57
+ * inside this plugin and reached `listByIds` directly; it owns its routes
58
+ * now, and identity lookup is the one thing it still wants from here.
59
+ */
60
+ adminUserService: AdminUserService;
61
+ }
62
+ /**
63
+ * Admin module composition root. Returns a handle so tests + production
64
+ * boot code can subscribe other modules to AuditPort etc.
65
+ */
66
+ export declare function adminModule(options: AdminModuleOptions): {
67
+ plugin: (app: FastifyInstance) => Promise<void>;
68
+ handle: AdminModuleHandle;
69
+ };
70
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,aAAa,EACb,eAAe,EACf,uBAAuB,EACvB,YAAY,EACZ,uBAAuB,EACvB,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAIlE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAEpF;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,6EAA6E;IAC7E,eAAe,EAAE,eAAe,CAAC;IACjC,eAAe,EAAE,SAAS,CAAC;IAC3B,wEAAwE;IACxE,iBAAiB,EAAE,kBAAkB,CAAC;IACtC,gEAAgE;IAChE,uBAAuB,EAAE,uBAAuB,CAAC;IACjD;;;;;OAKG;IACH,aAAa,EAAE,aAAa,CAAC;IAC7B,qEAAqE;IACrE,uBAAuB,EAAE,uBAAuB,CAAC;IACjD,YAAY,EAAE,mBAAmB,CAAC;IAClC;uEACmE;IACnE,mBAAmB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,6EAA6E;IAC7E,eAAe,CAAC,EAAE,MAAM,YAAY,GAAG,SAAS,CAAC;IACjD;;;;OAIG;IACH,mBAAmB,EAAE,wBAAwB,CAAC;CAC/C;AAED,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,iBAAiB,EAAE,kBAAkB,CAAC;IACtC,eAAe,EAAE,SAAS,CAAC;IAC3B;;;;;OAKG;IACH,gBAAgB,EAAE,gBAAgB,CAAC;CACpC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,kBAAkB,GAC1B;IAAE,MAAM,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAqDhF"}
@@ -0,0 +1,50 @@
1
+ import { AdminAuthService } from './services/admin-auth-service.js';
2
+ import { ImpersonationService } from './services/impersonation-service.js';
3
+ import { AdminUserService } from './services/admin-user-service.js';
4
+ import { registerAdminPublicRoutes } from './routes.public.js';
5
+ import { registerImpersonationRoutes } from './routes.impersonation.js';
6
+ import { registerAdminUsersAdminRoutes } from './routes.admin.js';
7
+ /**
8
+ * Admin module composition root. Returns a handle so tests + production
9
+ * boot code can subscribe other modules to AuditPort etc.
10
+ */
11
+ export function adminModule(options) {
12
+ const adminAuthService = new AdminAuthService(options.emFactory, options.authSessionPort, options.getMfaLoginPort, options.auditLogService);
13
+ const impersonationService = new ImpersonationService(options.emFactory, options.authSessionPort, options.customerAccountReadPort, options.auditLogService);
14
+ const adminUserService = new AdminUserService(options.emFactory, options.adminRolePort, options.authSessionPort, options.auditLogService);
15
+ const handle = {
16
+ adminAuthService,
17
+ impersonationService,
18
+ permissionService: options.permissionService,
19
+ auditLogService: options.auditLogService,
20
+ adminUserService,
21
+ };
22
+ return {
23
+ handle,
24
+ plugin: async (app) => {
25
+ await registerAdminPublicRoutes(app, {
26
+ adminAuthService,
27
+ twoFactorEnrolments: options.twoFactorEnrolments,
28
+ });
29
+ await registerImpersonationRoutes(app, {
30
+ impersonationService,
31
+ requireAdmin: options.requireAdmin,
32
+ // The production actor. This route used to read `request.testActor`
33
+ // and 401 whenever it was absent, which is every production request
34
+ // (feature 080, T051) — the module already had the right resolver in
35
+ // hand for `GET /admin/me`.
36
+ resolveAdminUserId: (req) => options.resolveAdminContext(req).adminUserId,
37
+ });
38
+ await registerAdminUsersAdminRoutes(app, {
39
+ adminUserService,
40
+ adminRolePort: options.adminRolePort,
41
+ permissionCataloguePort: options.permissionCataloguePort,
42
+ permissionService: options.permissionService,
43
+ requireAdmin: options.requireAdmin,
44
+ resolveAdminContext: options.resolveAdminContext,
45
+ twoFactorEnrolments: options.twoFactorEnrolments,
46
+ });
47
+ },
48
+ };
49
+ }
50
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEpE,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAwDlE;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,OAA2B;IAE3B,MAAM,gBAAgB,GAAG,IAAI,gBAAgB,CAC3C,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,eAAe,EACvB,OAAO,CAAC,eAAe,EACvB,OAAO,CAAC,eAAe,CACxB,CAAC;IACF,MAAM,oBAAoB,GAAG,IAAI,oBAAoB,CACnD,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,eAAe,EACvB,OAAO,CAAC,uBAAuB,EAC/B,OAAO,CAAC,eAAe,CACxB,CAAC;IACF,MAAM,gBAAgB,GAAG,IAAI,gBAAgB,CAC3C,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,aAAa,EACrB,OAAO,CAAC,eAAe,EACvB,OAAO,CAAC,eAAe,CACxB,CAAC;IACF,MAAM,MAAM,GAAsB;QAChC,gBAAgB;QAChB,oBAAoB;QACpB,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;QAC5C,eAAe,EAAE,OAAO,CAAC,eAAe;QACxC,gBAAgB;KACjB,CAAC;IACF,OAAO;QACL,MAAM;QACN,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACpB,MAAM,yBAAyB,CAAC,GAAG,EAAE;gBACnC,gBAAgB;gBAChB,mBAAmB,EAAE,OAAO,CAAC,mBAAmB;aACjD,CAAC,CAAC;YACH,MAAM,2BAA2B,CAAC,GAAG,EAAE;gBACrC,oBAAoB;gBACpB,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,oEAAoE;gBACpE,oEAAoE;gBACpE,qEAAqE;gBACrE,4BAA4B;gBAC5B,kBAAkB,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,WAAW;aAC1E,CAAC,CAAC;YACH,MAAM,6BAA6B,CAAC,GAAG,EAAE;gBACvC,gBAAgB;gBAChB,aAAa,EAAE,OAAO,CAAC,aAAa;gBACpC,uBAAuB,EAAE,OAAO,CAAC,uBAAuB;gBACxD,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;gBAC5C,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,mBAAmB,EAAE,OAAO,CAAC,mBAAmB;gBAChD,mBAAmB,EAAE,OAAO,CAAC,mBAAmB;aACjD,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
2
+ import { type AdminRolePort, type PermissionCataloguePort, type PermissionReadPort } from '@endora-commerce/contracts';
3
+ import type { AdminUserService } from './services/admin-user-service.js';
4
+ import type { TwoFactorEnrolmentReader } from './services/two-factor-enrolments.js';
5
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
6
+ /**
7
+ * Admin user + role CRUD (T193 / FR-080..FR-083). All gated by
8
+ * `admin_users:manage`. The wildcard `*` is intentionally not exposed
9
+ * by this surface — bootstrap-only.
10
+ */
11
+ export interface AdminUsersAdminDeps {
12
+ adminUserService: AdminUserService;
13
+ adminRolePort: AdminRolePort;
14
+ permissionCataloguePort: PermissionCataloguePort;
15
+ permissionService: PermissionReadPort;
16
+ requireAdmin: RequireAdminFactory;
17
+ /** Resolves the current admin's id — reads `request.actor` in production
18
+ * and `request.testActor` under the test harness. */
19
+ resolveAdminContext: (req: FastifyRequest) => {
20
+ adminUserId: string;
21
+ };
22
+ /**
23
+ * `mfa`'s answer to "which of these admins hold a second factor", already
24
+ * narrowed to this module's subject type. `twoFactorEnabled` used to be
25
+ * `!!u.twoFactorConfirmedAt` — a column on this module's own table that has
26
+ * never been written — so the 2FA column on `/admin-users` read `—` for an
27
+ * admin who had enrolled an hour earlier.
28
+ */
29
+ twoFactorEnrolments: TwoFactorEnrolmentReader;
30
+ }
31
+ export declare function registerAdminUsersAdminRoutes(app: FastifyInstance, deps: AdminUsersAdminDeps): Promise<void>;
32
+ //# 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,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAML,KAAK,aAAa,EAElB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AACpF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;GAIG;AAEH,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,aAAa,EAAE,aAAa,CAAC;IAC7B,uBAAuB,EAAE,uBAAuB,CAAC;IACjD,iBAAiB,EAAE,kBAAkB,CAAC;IACtC,YAAY,EAAE,mBAAmB,CAAC;IAClC;0DACsD;IACtD,mBAAmB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;;;OAMG;IACH,mBAAmB,EAAE,wBAAwB,CAAC;CAC/C;AAED,wBAAsB,6BAA6B,CACjD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,IAAI,CAAC,CAiOf"}
@@ -0,0 +1,192 @@
1
+ import { createAdminUserRequestSchema, resetAdminUserPasswordRequestSchema, updateAdminUserRequestSchema, updateAdminUserSelfRequestSchema, upsertAdminRoleRequestSchema, } from '@endora-commerce/contracts';
2
+ export async function registerAdminUsersAdminRoutes(app, deps) {
3
+ const { adminUserService, adminRolePort, permissionCataloguePort, permissionService, requireAdmin, resolveAdminContext, twoFactorEnrolments, } = deps;
4
+ /** One `mfa` read per response, whether the response carries one admin or a page. */
5
+ async function serializeAdminUsers(users) {
6
+ if (users.length === 0)
7
+ return [];
8
+ const enrolled = await twoFactorEnrolments(users.map((u) => u.id));
9
+ return users.map((u) => serializeAdminUser(u, enrolled.has(u.id)));
10
+ }
11
+ async function serializeOneAdminUser(user) {
12
+ return (await serializeAdminUsers([user]))[0];
13
+ }
14
+ // --- Current admin (for the UI auth gate) ---------------------------------
15
+ app.get('/api/v1/admin/me', { preHandler: requireAdmin() }, async (request) => {
16
+ const ctx = resolveAdminContext(request);
17
+ const adminUser = await adminUserService.getById(ctx.adminUserId);
18
+ const permissions = await permissionService.listPermissions(adminUser.id);
19
+ // No `.catch(() => null)` around the port call (feature 075, Phase C,
20
+ // FR-032): it would swallow `ModuleDisabledError` and answer "this admin
21
+ // has no role" for "`admin_roles` is not here", which is the fail-open
22
+ // the gate exists to prevent. Nothing is lost by dropping it —
23
+ // `admin_users_admin_role_fk` is `on delete restrict`
24
+ // (`migrations/20260425T053028_admin_users_init.ts:41`), so a non-null
25
+ // `adminRoleId` always names a live role.
26
+ const role = adminUser.adminRoleId
27
+ ? await adminRolePort.getById(adminUser.adminRoleId)
28
+ : null;
29
+ return {
30
+ data: {
31
+ adminUser: await serializeOneAdminUser(adminUser),
32
+ role: role ? serializeAdminRole(role) : null,
33
+ permissions,
34
+ },
35
+ };
36
+ });
37
+ // --- Self profile edit ------------------------------------------------------
38
+ // Any authenticated admin can edit their own first/last name and rotate
39
+ // their password without holding `admin_users:manage`. Role + status
40
+ // changes are intentionally NOT exposed here.
41
+ app.patch('/api/v1/admin/me', {
42
+ preHandler: requireAdmin(),
43
+ schema: { body: updateAdminUserSelfRequestSchema },
44
+ }, async (request) => {
45
+ const ctx = resolveAdminContext(request);
46
+ const body = updateAdminUserSelfRequestSchema.parse(request.body);
47
+ const user = await adminUserService.update(ctx.adminUserId, {
48
+ ...(body.firstName !== undefined ? { firstName: body.firstName } : {}),
49
+ ...(body.lastName !== undefined ? { lastName: body.lastName } : {}),
50
+ ...(body.password !== undefined ? { password: body.password } : {}),
51
+ });
52
+ return { data: await serializeOneAdminUser(user) };
53
+ });
54
+ // --- Permissions catalogue -------------------------------------------------
55
+ app.get('/api/v1/admin/permissions', { preHandler: requireAdmin('admin_users:manage') }, async () => ({ data: permissionCataloguePort.listAssignable() }));
56
+ // --- Admin users -----------------------------------------------------------
57
+ app.get('/api/v1/admin/admin-users', { preHandler: requireAdmin('admin_users:manage') }, async (request) => {
58
+ const q = (request.query ?? {});
59
+ const search = q['q']?.trim();
60
+ const result = await adminUserService.list({
61
+ ...(search ? { q: search } : {}),
62
+ ...(q['page'] ? { page: Number.parseInt(q['page'], 10) } : {}),
63
+ ...(q['pageSize'] ? { pageSize: Number.parseInt(q['pageSize'], 10) } : {}),
64
+ });
65
+ return {
66
+ data: await serializeAdminUsers(result.items),
67
+ pagination: {
68
+ page: result.page,
69
+ pageSize: result.pageSize,
70
+ total: result.total,
71
+ cursor: null,
72
+ hasMore: (result.page + 1) * result.pageSize < result.total,
73
+ limit: result.items.length,
74
+ },
75
+ };
76
+ });
77
+ app.post('/api/v1/admin/admin-users', {
78
+ preHandler: requireAdmin('admin_users:manage'),
79
+ schema: { body: createAdminUserRequestSchema },
80
+ }, async (request, reply) => {
81
+ const body = createAdminUserRequestSchema.parse(request.body);
82
+ const user = await adminUserService.create({
83
+ email: body.email,
84
+ password: body.password,
85
+ firstName: body.firstName,
86
+ lastName: body.lastName,
87
+ ...(body.adminRoleId !== undefined ? { adminRoleId: body.adminRoleId } : {}),
88
+ });
89
+ reply.status(201);
90
+ return { data: await serializeOneAdminUser(user) };
91
+ });
92
+ app.patch('/api/v1/admin/admin-users/:id', {
93
+ preHandler: requireAdmin('admin_users:manage'),
94
+ schema: { body: updateAdminUserRequestSchema },
95
+ }, async (request) => {
96
+ const body = updateAdminUserRequestSchema.parse(request.body);
97
+ const user = await adminUserService.update(request.params.id, {
98
+ ...(body.firstName !== undefined ? { firstName: body.firstName } : {}),
99
+ ...(body.lastName !== undefined ? { lastName: body.lastName } : {}),
100
+ ...(body.adminRoleId !== undefined ? { adminRoleId: body.adminRoleId } : {}),
101
+ ...(body.status !== undefined ? { status: body.status } : {}),
102
+ });
103
+ return { data: await serializeOneAdminUser(user) };
104
+ });
105
+ // --- Peer password reset (issue #252) ---------------------------------------
106
+ //
107
+ // The capability `admin_users`' manifest has always promised. Gated by
108
+ // `admin_users:manage`, the same code that already gates creating an admin
109
+ // user and assigning it any role — including the role holding `*`. A
110
+ // narrower code would be absent from every role on every deployment that
111
+ // exists today, so shipping one would leave the lockout it closes open until
112
+ // somebody with `*` edited the roles, which is the bootstrap problem again.
113
+ //
114
+ // An operator holding the code may target themselves. There is no role
115
+ // hierarchy here to rank a reset against, and the same code already assigns
116
+ // any role including the one holding `*`, so a self-target guard would close
117
+ // nothing — while making this route disagree with the e-mail-keyed flow that
118
+ // follows, which is self-targeted by construction.
119
+ //
120
+ // Own path rather than a `password` field on the PATCH: setting a password
121
+ // has its own audit action and revokes the target's sessions, and a request
122
+ // that renamed and reset in one call would have to answer for both.
123
+ // `/password-reset` is deliberately left free for the e-mail-keyed flow that
124
+ // follows, so the two never collide on one verb — this one **sets** a
125
+ // password, that one will **send** a link, exactly as
126
+ // `POST /api/v1/admin/customers/:id/password-reset` already does.
127
+ app.post('/api/v1/admin/admin-users/:id/password', {
128
+ preHandler: requireAdmin('admin_users:manage'),
129
+ schema: { body: resetAdminUserPasswordRequestSchema },
130
+ }, async (request) => {
131
+ const body = resetAdminUserPasswordRequestSchema.parse(request.body);
132
+ const user = await adminUserService.resetPassword(request.params.id, body.password);
133
+ return { data: await serializeOneAdminUser(user) };
134
+ });
135
+ app.delete('/api/v1/admin/admin-users/:id', { preHandler: requireAdmin('admin_users:manage') }, async (request, reply) => {
136
+ await adminUserService.softDelete(request.params.id);
137
+ return reply.status(204).send();
138
+ });
139
+ // --- Admin roles -----------------------------------------------------------
140
+ app.get('/api/v1/admin/admin-roles', { preHandler: requireAdmin('admin_users:manage') }, async () => {
141
+ const rows = await adminRolePort.list();
142
+ return { data: rows.map(serializeAdminRole) };
143
+ });
144
+ app.put('/api/v1/admin/admin-roles/:code', {
145
+ preHandler: requireAdmin('admin_users:manage'),
146
+ schema: { body: upsertAdminRoleRequestSchema },
147
+ }, async (request) => {
148
+ const body = upsertAdminRoleRequestSchema.parse(request.body);
149
+ const role = await adminRolePort.upsertByCode({
150
+ code: request.params.code,
151
+ name: body.name,
152
+ permissions: body.permissions,
153
+ ...(body.requiresTwoFactor !== undefined
154
+ ? { requiresTwoFactor: body.requiresTwoFactor }
155
+ : {}),
156
+ });
157
+ return { data: serializeAdminRole(role) };
158
+ });
159
+ app.delete('/api/v1/admin/admin-roles/:id', { preHandler: requireAdmin('admin_users:manage') }, async (request, reply) => {
160
+ await adminRolePort.remove(request.params.id);
161
+ return reply.status(204).send();
162
+ });
163
+ }
164
+ function serializeAdminUser(u, twoFactorEnabled) {
165
+ return {
166
+ id: u.id,
167
+ email: u.email,
168
+ firstName: u.firstName,
169
+ lastName: u.lastName,
170
+ adminRoleId: u.adminRoleId ?? null,
171
+ twoFactorEnabled,
172
+ status: u.status,
173
+ // Feature 019: surface the per-user Admin UI language preference so the
174
+ // SPA's TranslationProvider can seed itself without a separate fetch.
175
+ preferredLanguage: u.preferredLanguage ?? null,
176
+ lastLoginAt: u.lastLoginAt?.toISOString() ?? null,
177
+ createdAt: u.createdAt.toISOString(),
178
+ updatedAt: u.updatedAt.toISOString(),
179
+ };
180
+ }
181
+ function serializeAdminRole(r) {
182
+ return {
183
+ id: r.id,
184
+ code: r.code,
185
+ name: r.name,
186
+ permissions: r.permissions,
187
+ requiresTwoFactor: r.requiresTwoFactor,
188
+ createdAt: r.createdAt.toISOString(),
189
+ updatedAt: r.updatedAt.toISOString(),
190
+ };
191
+ }
192
+ //# sourceMappingURL=routes.admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.admin.js","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
2
+ import type { ImpersonationService } from './services/impersonation-service.js';
3
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
4
+ export interface ImpersonationDeps {
5
+ impersonationService: ImpersonationService;
6
+ requireAdmin: RequireAdminFactory;
7
+ /**
8
+ * The acting admin's id, resolved from the request the
9
+ * `requireAdmin('customers:impersonate')` guard has already accepted.
10
+ *
11
+ * It used to be read here from `request.testActor`, narrowed through
12
+ * `http/test-actor-carrier` — a file `contracts/host-package.md` §1.4j
13
+ * classifies **A**, because it exists to keep this repository's test-harness
14
+ * Fastify augmentation out of production code and an installed package has no
15
+ * relationship to that harness. Nothing under `src/` writes that field, so the
16
+ * inline check answered `401 Admin session required` to every production
17
+ * request while the guard in front of it was doing the real gating.
18
+ *
19
+ * Both composition roots supply it from `adminContextResolver`, which reads
20
+ * the production actor and throws 401 for a non-admin.
21
+ */
22
+ resolveAdminUserId: (request: FastifyRequest) => string;
23
+ }
24
+ export declare function registerImpersonationRoutes(app: FastifyInstance, deps: ImpersonationDeps): Promise<void>;
25
+ //# sourceMappingURL=routes.impersonation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.impersonation.d.ts","sourceRoot":"","sources":["../../src/backend/routes.impersonation.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAgB,cAAc,EAAE,MAAM,SAAS,CAAC;AAQ7E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAChF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAiB5E,MAAM,WAAW,iBAAiB;IAChC,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;;;;;;;;;;OAcG;IACH,kBAAkB,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,MAAM,CAAC;CACzD;AAED,wBAAsB,2BAA2B,CAC/C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,IAAI,CAAC,CAqFf"}