@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,182 @@
1
+ import { type ModuleCliCommand } from '@endora-commerce/contracts';
2
+ import type { ModuleContext } from '@endora-commerce/platform/kernel';
3
+ /**
4
+ * Admin Users module — manifest backfill (Module Lifecycle, feature 018).
5
+ *
6
+ * Predates the lifecycle system; this manifest is the static record
7
+ * required so the module participates in the registry. No install /
8
+ * uninstall hook today — the module's schema is owned by earlier
9
+ * platform-wide migrations.
10
+ */
11
+ export declare const manifest: {
12
+ id: string;
13
+ name: string;
14
+ version: string;
15
+ dependencies: string[];
16
+ description?: string | undefined;
17
+ acknowledgedDependencies?: {
18
+ moduleId: string;
19
+ port: string;
20
+ reason: string;
21
+ }[] | undefined;
22
+ nonBindingDependencies?: {
23
+ moduleId: string;
24
+ name: string;
25
+ kind: "contributes-to" | "degrades-without" | "refuses-without";
26
+ reason: string;
27
+ whenAbsent?: string | undefined;
28
+ }[] | undefined;
29
+ activation?: {
30
+ settingCode: string;
31
+ default: boolean;
32
+ } | {
33
+ nonDeactivatable: true;
34
+ reason: string;
35
+ } | undefined;
36
+ settings?: {
37
+ moduleCode: string;
38
+ groups: {
39
+ code: string;
40
+ name: string;
41
+ salesChannelCodes?: string[] | undefined;
42
+ isSystemProtected?: boolean | undefined;
43
+ }[];
44
+ settings: {
45
+ code: string;
46
+ name: string;
47
+ valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
48
+ defaultValue: unknown;
49
+ description?: string | undefined;
50
+ groupCode?: string | undefined;
51
+ previousDefaultValues?: unknown[] | undefined;
52
+ salesChannelCodes?: string[] | undefined;
53
+ enumOptions?: string[] | undefined;
54
+ configurationType?: string | undefined;
55
+ hidden?: boolean | undefined;
56
+ }[];
57
+ } | undefined;
58
+ i18n?: {
59
+ bundlesDir: string;
60
+ } | undefined;
61
+ docs?: false | {
62
+ dir: string;
63
+ } | undefined;
64
+ demo?: false | {
65
+ summary: string;
66
+ seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
67
+ reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
68
+ after?: readonly string[] | undefined;
69
+ package?: string | undefined;
70
+ } | undefined;
71
+ actions?: {
72
+ id: string;
73
+ labelKey: string;
74
+ icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
75
+ targetRoute: string;
76
+ keywords: string[];
77
+ weight: number;
78
+ descriptionKey?: string | undefined;
79
+ requiredPermission?: string | undefined;
80
+ }[] | undefined;
81
+ permissions?: {
82
+ code: string;
83
+ label: string;
84
+ module?: string | undefined;
85
+ description?: string | undefined;
86
+ requires?: string[] | undefined;
87
+ }[] | undefined;
88
+ transactionalEmails?: {
89
+ code: string;
90
+ name: string;
91
+ variables: {
92
+ key: string;
93
+ label: string;
94
+ sampleValue?: string | undefined;
95
+ description?: string | undefined;
96
+ }[];
97
+ description?: string | undefined;
98
+ group?: string | undefined;
99
+ }[] | undefined;
100
+ capabilities?: string[] | undefined;
101
+ exclusiveCapabilities?: {
102
+ key: string;
103
+ errorCode: string;
104
+ }[] | undefined;
105
+ errorCodes?: {
106
+ code: string;
107
+ tokens?: string[] | undefined;
108
+ }[] | undefined;
109
+ blocks?: {
110
+ name: string;
111
+ labelKey: string;
112
+ category: string;
113
+ contexts: ("email" | "invoice" | "cms" | "newsletter")[];
114
+ fields: Record<string, {
115
+ type: "number" | "object" | "array" | "uuid" | "text" | "textarea" | "select" | "radio" | "external" | "richtext";
116
+ label?: string | undefined;
117
+ required?: boolean | undefined;
118
+ options?: {
119
+ label: string;
120
+ value: string | number;
121
+ }[] | undefined;
122
+ refKind?: string | undefined;
123
+ }>;
124
+ descriptionKey?: string | undefined;
125
+ defaultProps?: Record<string, unknown> | undefined;
126
+ responsiveFields?: string[] | undefined;
127
+ previewIcon?: string | undefined;
128
+ weight?: number | undefined;
129
+ }[] | undefined;
130
+ blockCategories?: {
131
+ key: string;
132
+ titleKey: string;
133
+ contexts: ("email" | "invoice" | "cms" | "newsletter")[];
134
+ weight?: number | undefined;
135
+ visible?: boolean | undefined;
136
+ }[] | undefined;
137
+ env?: {
138
+ name: string;
139
+ describes: {
140
+ en: string;
141
+ pl: string;
142
+ };
143
+ requirement: {
144
+ kind: "required";
145
+ } | {
146
+ kind: "requiredWhen";
147
+ input: string;
148
+ equals: string;
149
+ } | {
150
+ kind: "optional";
151
+ without: {
152
+ en: string;
153
+ pl: string;
154
+ };
155
+ };
156
+ secret: boolean;
157
+ generable: boolean;
158
+ owner: {
159
+ kind: "platform";
160
+ } | {
161
+ kind: "application";
162
+ application: "admin" | "backend" | "storefront";
163
+ } | {
164
+ kind: "module";
165
+ moduleId: string;
166
+ };
167
+ consumers: ("admin" | "backend" | "storefront")[];
168
+ addressOf: "admin" | "backend" | "storefront" | null;
169
+ }[] | undefined;
170
+ };
171
+ /**
172
+ * The operator command this module declares — feature 080, T042b / D-160.9.
173
+ *
174
+ * It was `scripts/create-admin.ts`, whose one `check:module-boundary` key was an
175
+ * `AdminRole` **entity** import: with no container there was nothing to resolve
176
+ * `adminRolePort` from. Composing reaches it, and D-157.5 is why that is safe on
177
+ * a database with no administrator in it — `loadModulePresence` runs before the
178
+ * first module registers and its reconciler is what puts the presence rows
179
+ * there, so a composed bootstrap resolves a working port on a virgin schema.
180
+ */
181
+ export declare const cliCommands: ReadonlyArray<ModuleCliCommand<ModuleContext>>;
182
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AA8BtE;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuJnB,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,EAAE,aAAa,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAetE,CAAC"}
@@ -0,0 +1,208 @@
1
+ import { defineModuleManifest, } from '@endora-commerce/contracts';
2
+ /**
3
+ * The demo data this module owns (feature 113, T222 — contract §1.3).
4
+ *
5
+ * A typed `const` rather than an inline object literal: declared inline the
6
+ * parameter infers from the schema and is `never`, so the author loses
7
+ * `context.ctx: ModuleContext`.
8
+ *
9
+ * Both bodies are reached by a **relative `await import()`** (§1.4), in
10
+ * `cliCommands.run`'s shape and for `cliCommands`' reason: a manifest is loaded
11
+ * by every process that composes the platform and by the check scripts that
12
+ * import the generated index, so a demo body imported at the top of this file
13
+ * would be a service graph pulled into all of them. It needs no `exports`
14
+ * subpath and no `files` entry (§1.5).
15
+ *
16
+ * **The role each account holds is not here.** An `admin_users` row carrying an
17
+ * `admin_roles` id is two modules' rows in one statement, so the assignment is a
18
+ * composition step (§5.1) and belongs to whoever owns the instance. The column
19
+ * is nullable, which is what makes that split available at all.
20
+ *
21
+ * The sign-in details come back as `DemoSeedResult.credentials`, so the runner
22
+ * formats them once (§3.7) instead of the seed printing them itself.
23
+ */
24
+ const demo = {
25
+ summary: 'Three administrator accounts to sign in to the Admin UI with.',
26
+ seed: async (context) => (await import('./backend/demo/seed.js')).seedDemo(context),
27
+ reset: async (context) => (await import('./backend/demo/reset.js')).resetDemo(context),
28
+ };
29
+ /**
30
+ * Admin Users module — manifest backfill (Module Lifecycle, feature 018).
31
+ *
32
+ * Predates the lifecycle system; this manifest is the static record
33
+ * required so the module participates in the registry. No install /
34
+ * uninstall hook today — the module's schema is owned by earlier
35
+ * platform-wide migrations.
36
+ */
37
+ export const manifest = defineModuleManifest({
38
+ id: 'admin_users',
39
+ name: 'Admin Users',
40
+ description: 'Admin user accounts, sessions, and impersonation flows.',
41
+ version: '1.0.0',
42
+ dependencies: ['admin_roles', 'auth'],
43
+ /**
44
+ * Feature 075, Phase C — impersonation resolves its target through
45
+ * `customerAccountReadPort` instead of querying `customer_accounts`' entity.
46
+ *
47
+ * It is acknowledged rather than declared, because the ordinary declaration
48
+ * closes a cycle `module-graph.test.ts` and the composer generator both refuse:
49
+ * `customer_accounts` → `price_lists` → `catalog` → `admin_users`. The same
50
+ * shape `auth` records against `customer_accounts` and `admin_roles` against
51
+ * this module.
52
+ *
53
+ * The seam fails closed — the port is gated, and an impersonation that cannot
54
+ * identify its target must refuse rather than mint a session against a
55
+ * customer nobody looked up. That is what the edge costs, stated as a
56
+ * behaviour rather than as a claim about which flips are refused: the locked
57
+ * set is re-derived on every run, and a sentence here is a copy nothing
58
+ * refreshes (D-100).
59
+ */
60
+ acknowledgedDependencies: [
61
+ {
62
+ moduleId: 'customer_accounts',
63
+ port: 'customerAccountReadPort',
64
+ reason: 'Starting an impersonation reads the target customer account — its organisation ' +
65
+ 'membership and whether it is still live — and that row belongs to customer_accounts, ' +
66
+ 'which reaches this module transitively through price_lists and catalog. Declaring it ' +
67
+ 'here closes a cycle. With customer_accounts absent the seam fails closed: an ' +
68
+ 'impersonation that cannot identify its target refuses rather than minting a session ' +
69
+ 'against a customer nobody looked up.',
70
+ },
71
+ ],
72
+ /**
73
+ * D-96 — `mfaLoginPort`, the second factor on admin login.
74
+ *
75
+ * Real to the container, binding on no operator. `mfa` declares this module
76
+ * in its own `dependencies`, so the ordinary declaration closes a cycle; and
77
+ * an acknowledged edge would put admin login among the dependents that refuse
78
+ * the flip, which is the wrong way round — two-factor authentication is a
79
+ * client security policy, not a platform floor, and feature 074 gave the
80
+ * operator a switch for exactly that reason.
81
+ */
82
+ nonBindingDependencies: [
83
+ {
84
+ moduleId: 'mfa',
85
+ name: 'mfaLoginPort',
86
+ kind: 'degrades-without',
87
+ whenAbsent: 'Admin sign-in stops asking for a second factor and offers no Google/Microsoft button. ' +
88
+ 'Every admin has a password a peer admin can reset, so no admin is locked out.',
89
+ reason: 'AdminAuthService verifies the password first and then asks the second factor what to ' +
90
+ 'do. With `mfa` absent it asks nobody: `backend.ts` probes ' +
91
+ '`effectiveState.isPresent("mfa")` and passes `undefined`, which selects the ' +
92
+ 'password-only branch feature 042 FR-033 requires and this service has always had. ' +
93
+ 'Nothing catches `ModuleDisabledError` — the decision is taken before the port is ' +
94
+ 'resolved, so the degrade is declared rather than laundered out of a closed gate. ' +
95
+ 'Enrolled secrets, recovery codes and every policy value stay in the database and ' +
96
+ 'apply again on the way back, which is what the activation control promises. Admin ' +
97
+ 'accounts are never auto-created and always carry a human-chosen password a peer ' +
98
+ 'admin can reset, so no admin becomes unreachable while the module is off.',
99
+ },
100
+ {
101
+ moduleId: 'mfa',
102
+ name: 'mfaEnrolmentStatePort',
103
+ kind: 'degrades-without',
104
+ whenAbsent: 'Reads "no second factor" for every admin on /admin-users, on /admin/me and on the ' +
105
+ 'login response. True while MFA is off — admin sign-in asks for none — and not a ' +
106
+ 'claim that nobody is enrolled.',
107
+ reason: '`twoFactorEnabled` on every response carrying an admin user is the live `mfa` ' +
108
+ 'enrolment, read in one batch through `mfaEnrolmentStatePort`. It was ' +
109
+ '`!!u.twoFactorConfirmedAt` until 2026-08-28 — a column on this module\'s own table ' +
110
+ 'that has never had a writer — so the screen whose job is to tell an operator who is ' +
111
+ 'protected answered "nobody" while people were protected. Non-binding for the reason ' +
112
+ 'the login edge beside it is: `mfa` declares this module in its own `dependencies`, ' +
113
+ 'so an ordinary declaration closes a cycle, and an acknowledged edge would make a ' +
114
+ 'client security policy unswitchable over a column on a list screen. The degrade is ' +
115
+ 'taken by **not resolving** — `backend.ts` probes presence first and answers an empty ' +
116
+ 'set — so nothing catches `ModuleDisabledError`.',
117
+ },
118
+ ],
119
+ // D-173 — `customers:impersonate` is a **shared** gate, in the shape issue
120
+ // #213 gave `integrations:manage`: it guards this module's
121
+ // `/admin/organizations/:id/impersonate` and `/admin/impersonation/end`
122
+ // endpoints and `customers`' own impersonation start, and the core
123
+ // `PERMISSION_CATALOGUE` row that carries its label names `customers` alone.
124
+ // Declaring it here makes this module a second *owner*, so the presence
125
+ // filter on `/admin-roles` keeps it grantable while either surface is on.
126
+ // Without this line, switching `customers` off would take the code off the
127
+ // role editor while these routes — owned by a module that declares itself
128
+ // non-deactivatable — went on enforcing it: a gate nobody can be granted.
129
+ //
130
+ // The label decides which repair applies: *"Impersonate customers"* reads as
131
+ // a sentence about this module's own screen, so the code is shared rather
132
+ // than replaced by one of this module's own.
133
+ /**
134
+ * This module's first i18n bundle and its first command-palette action —
135
+ * feature 091, Phase 4, batch four.
136
+ *
137
+ * The bundle exists because the sidebar entry moved into this package with
138
+ * the two screens and a nav declaration's `labelKey` is **module-relative**
139
+ * (R8): `appShell.nav.users` was one of `_i18n`'s and is
140
+ * `nav.adminUsers.label` here. It is a **flat** `{"a.b.c": "text"}` map at
141
+ * the **package root**, for the reason `admin_roles`' bundle records.
142
+ *
143
+ * The action pays one of the fifteen entries `specs/deferred-defects.md`
144
+ * still holds under *"Sixteen modules with an admin screen declare no
145
+ * command-palette action"*. `admin_users:manage` is the code every endpoint
146
+ * under `/api/v1/admin/admin-users` enforces, which is what
147
+ * `check:action-route-permissions` holds this declaration to.
148
+ *
149
+ * **There is exactly one action and it points at `/admin-users`, not at
150
+ * `/admin-roles`.** This module declares the roles editor's *route* — the
151
+ * screen's API is its own — but the roles *advertisement* is `admin_roles`',
152
+ * which ships the sidebar entry and the palette action for it. See
153
+ * `src/admin/index.ts` for the split and for why it is inert while both
154
+ * modules are locked.
155
+ */
156
+ i18n: { bundlesDir: 'i18n' },
157
+ docs: { dir: 'docs' },
158
+ actions: [
159
+ {
160
+ id: 'open-admin-users',
161
+ labelKey: 'actions.openAdminUsers.label',
162
+ descriptionKey: 'actions.openAdminUsers.description',
163
+ icon: 'Users',
164
+ targetRoute: '/admin-users',
165
+ requiredPermission: 'admin_users:manage',
166
+ keywords: ['admin', 'users', 'uzytkownicy', 'operators', 'operatorzy', 'staff', 'konta'],
167
+ weight: 200,
168
+ },
169
+ ],
170
+ permissions: [{ code: 'customers:impersonate', label: 'Impersonate customers' }],
171
+ demo,
172
+ // Feature 072/073 (Constitution XVII) — this module owns the admin login
173
+ // route, the admin session and the impersonation flow. Switched off, nobody
174
+ // can sign in to the Admin UI, including to switch it back on: the one
175
+ // control that would undo the change is behind the door it just locked.
176
+ activation: {
177
+ nonDeactivatable: true,
178
+ reason: 'Owns admin login, sessions and impersonation; switched off, no operator could sign in ' +
179
+ 'to the Admin UI at all — including to switch it back on.',
180
+ },
181
+ });
182
+ /**
183
+ * The operator command this module declares — feature 080, T042b / D-160.9.
184
+ *
185
+ * It was `scripts/create-admin.ts`, whose one `check:module-boundary` key was an
186
+ * `AdminRole` **entity** import: with no container there was nothing to resolve
187
+ * `adminRolePort` from. Composing reaches it, and D-157.5 is why that is safe on
188
+ * a database with no administrator in it — `loadModulePresence` runs before the
189
+ * first module registers and its reconciler is what puts the presence rows
190
+ * there, so a composed bootstrap resolves a working port on a virgin schema.
191
+ */
192
+ export const cliCommands = [
193
+ {
194
+ name: 'create',
195
+ summary: 'Create or update an admin user, bootstrapping the platform_admin role.',
196
+ help: `usage: admin_users create --email=<e> --password=<p> --first-name=<f> --last-name=<l>
197
+ [--role=<code>] [--skip-role-bootstrap]
198
+
199
+ Idempotent: re-running with the same email updates the password and the role
200
+ assignment. The first admin created gets the \`platform_admin\` role with the
201
+ wildcard \`*\` permission; narrower roles are defined from the Admin UI.
202
+
203
+ --role=<code> an existing role code (default: platform_admin)
204
+ --skip-role-bootstrap do not create platform_admin when it is missing`,
205
+ run: async (context) => (await import('./backend/cli/create-admin.js')).createAdmin(context),
206
+ },
207
+ ];
208
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Admin module — Phase 6 (T184). Creates admin_roles + admin_users tables.
4
+ * admin_users.admin_role_id is RESTRICT-deleted to keep audit references
5
+ * meaningful even after a Role is deactivated.
6
+ */
7
+ export declare class Migration20260425T053028AdminUsersInit extends Migration {
8
+ up(): Promise<void>;
9
+ down(): Promise<void>;
10
+ }
11
+ //# sourceMappingURL=20260425T053028_admin_users_init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260425T053028_admin_users_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260425T053028_admin_users_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;GAIG;AACH,qBAAa,sCAAuC,SAAQ,SAAS;IACpD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAwCnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAIrC"}
@@ -0,0 +1,51 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Admin module — Phase 6 (T184). Creates admin_roles + admin_users tables.
4
+ * admin_users.admin_role_id is RESTRICT-deleted to keep audit references
5
+ * meaningful even after a Role is deactivated.
6
+ */
7
+ export class Migration20260425T053028AdminUsersInit extends Migration {
8
+ async up() {
9
+ this.addSql(`
10
+ create table "admin_roles" (
11
+ "id" uuid not null,
12
+ "code" varchar(64) not null,
13
+ "name" varchar(160) not null,
14
+ "permissions" jsonb not null default '[]'::jsonb,
15
+ "requires_two_factor" boolean not null default false,
16
+ "created_at" timestamptz not null,
17
+ "updated_at" timestamptz not null,
18
+ constraint "admin_roles_pkey" primary key ("id"),
19
+ constraint "admin_roles_code_unique" unique ("code")
20
+ );
21
+ `);
22
+ this.addSql(`
23
+ create table "admin_users" (
24
+ "id" uuid not null,
25
+ "email" varchar(320) not null,
26
+ "password_hash" varchar(512) not null,
27
+ "first_name" varchar(120) not null,
28
+ "last_name" varchar(120) not null,
29
+ "admin_role_id" uuid null,
30
+ "status" varchar(16) not null default 'active',
31
+ "two_factor_secret" varchar(64) null,
32
+ "two_factor_confirmed_at" timestamptz null,
33
+ "last_login_at" timestamptz null,
34
+ "created_at" timestamptz not null,
35
+ "updated_at" timestamptz not null,
36
+ "deleted_at" timestamptz null,
37
+ constraint "admin_users_pkey" primary key ("id"),
38
+ constraint "admin_users_email_unique" unique ("email"),
39
+ constraint "admin_users_admin_role_fk" foreign key ("admin_role_id")
40
+ references "admin_roles" ("id") on delete restrict
41
+ );
42
+ `);
43
+ this.addSql('create index "admin_users_admin_role_id_index" on "admin_users" ("admin_role_id");');
44
+ this.addSql('create index "admin_users_status_index" on "admin_users" ("status");');
45
+ }
46
+ async down() {
47
+ this.addSql('drop table if exists "admin_users" cascade;');
48
+ this.addSql('drop table if exists "admin_roles" cascade;');
49
+ }
50
+ }
51
+ //# sourceMappingURL=20260425T053028_admin_users_init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260425T053028_admin_users_init.js","sourceRoot":"","sources":["../../src/migrations/20260425T053028_admin_users_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;GAIG;AACH,MAAM,OAAO,sCAAuC,SAAQ,SAAS;IAC1D,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;KAYX,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;KAoBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,oFAAoF,CAAC,CAAC;QAClG,IAAI,CAAC,MAAM,CAAC,sEAAsE,CAAC,CAAC;IACtF,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,6CAA6C,CAAC,CAAC;QAC3D,IAAI,CAAC,MAAM,CAAC,6CAA6C,CAAC,CAAC;IAC7D,CAAC;CACF"}
@@ -0,0 +1,55 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Fold `admin_users.email` to the form the code now stores and compares.
4
+ *
5
+ * `AdminUserService.create` folded the address and the login read compared it
6
+ * verbatim, so an operator created as `Operator.Mixed@example.com` had
7
+ * `operator.mixed@example.com` on record and could never sign in with the address
8
+ * they were handed: Postgres' `=` on `text` is case-sensitive. The code side of
9
+ * the repair makes every read and every write go through
10
+ * `normalizeEmailAddress`; this migration brings the rows written before it to
11
+ * the same form, so an address stored mixed-case stays reachable.
12
+ *
13
+ * The policy is the one the buyer-side twin of this defect settled on
14
+ * (`20260819T142837_customer_accounts_fold_email_case.ts`), and it is repeated
15
+ * rather than re-decided.
16
+ *
17
+ * ## What a collision means, and why it does not fail the deploy
18
+ *
19
+ * Two rows differing only in case are two operators that the fold turns into
20
+ * one address, and `admin_users_email_unique` will not hold both. The migration
21
+ * therefore **folds one row per address and leaves the rest exactly as they
22
+ * are** — it drops nothing, merges nothing and refuses nothing:
23
+ *
24
+ * - a row that **already holds the folded address** wins it, because it is the
25
+ * one every audit row, every assignment and every session has been resolving
26
+ * to by that address all along;
27
+ * - otherwise the **earliest-created** row wins it, since it is the operator
28
+ * with the longer history behind it;
29
+ * - every other row in the group keeps its stored spelling and is counted by
30
+ * the warning below.
31
+ *
32
+ * A stranded row is not data loss but it is not a working operator either: the
33
+ * login read folds, so nothing will match its spelling until an operator
34
+ * renames it. Which admin account somebody's audit trail belongs to is not a
35
+ * decision a migration running unattended may take, and failing the deploy
36
+ * would stop every other repair in the same release over a pair of rows a human
37
+ * has to look at anyway.
38
+ *
39
+ * The admin side has one aggravating circumstance the buyer side does not: it
40
+ * ships no e-mail-keyed password reset, so a stranded operator has no
41
+ * self-service way back in at all. That is what the warning is for — it names a
42
+ * row somebody has to go and fix.
43
+ *
44
+ * ## Measured before it was written
45
+ *
46
+ * Zero collisions and zero mixed-case rows across both developer databases
47
+ * (`b2b`: 3 admin users; `b2b_test`: 4). The rules above are therefore written
48
+ * for the environments this will meet later, not for a pair of rows anybody has
49
+ * today.
50
+ */
51
+ export declare class Migration20260819T155150AdminUsersFoldEmailCase extends Migration {
52
+ up(): Promise<void>;
53
+ down(): Promise<void>;
54
+ }
55
+ //# sourceMappingURL=20260819T155150_admin_users_fold_email_case.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260819T155150_admin_users_fold_email_case.d.ts","sourceRoot":"","sources":["../../src/migrations/20260819T155150_admin_users_fold_email_case.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,qBAAa,+CAAgD,SAAQ,SAAS;IAC7D,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IA+CnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAQrC"}
@@ -0,0 +1,106 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Fold `admin_users.email` to the form the code now stores and compares.
4
+ *
5
+ * `AdminUserService.create` folded the address and the login read compared it
6
+ * verbatim, so an operator created as `Operator.Mixed@example.com` had
7
+ * `operator.mixed@example.com` on record and could never sign in with the address
8
+ * they were handed: Postgres' `=` on `text` is case-sensitive. The code side of
9
+ * the repair makes every read and every write go through
10
+ * `normalizeEmailAddress`; this migration brings the rows written before it to
11
+ * the same form, so an address stored mixed-case stays reachable.
12
+ *
13
+ * The policy is the one the buyer-side twin of this defect settled on
14
+ * (`20260819T142837_customer_accounts_fold_email_case.ts`), and it is repeated
15
+ * rather than re-decided.
16
+ *
17
+ * ## What a collision means, and why it does not fail the deploy
18
+ *
19
+ * Two rows differing only in case are two operators that the fold turns into
20
+ * one address, and `admin_users_email_unique` will not hold both. The migration
21
+ * therefore **folds one row per address and leaves the rest exactly as they
22
+ * are** — it drops nothing, merges nothing and refuses nothing:
23
+ *
24
+ * - a row that **already holds the folded address** wins it, because it is the
25
+ * one every audit row, every assignment and every session has been resolving
26
+ * to by that address all along;
27
+ * - otherwise the **earliest-created** row wins it, since it is the operator
28
+ * with the longer history behind it;
29
+ * - every other row in the group keeps its stored spelling and is counted by
30
+ * the warning below.
31
+ *
32
+ * A stranded row is not data loss but it is not a working operator either: the
33
+ * login read folds, so nothing will match its spelling until an operator
34
+ * renames it. Which admin account somebody's audit trail belongs to is not a
35
+ * decision a migration running unattended may take, and failing the deploy
36
+ * would stop every other repair in the same release over a pair of rows a human
37
+ * has to look at anyway.
38
+ *
39
+ * The admin side has one aggravating circumstance the buyer side does not: it
40
+ * ships no e-mail-keyed password reset, so a stranded operator has no
41
+ * self-service way back in at all. That is what the warning is for — it names a
42
+ * row somebody has to go and fix.
43
+ *
44
+ * ## Measured before it was written
45
+ *
46
+ * Zero collisions and zero mixed-case rows across both developer databases
47
+ * (`b2b`: 3 admin users; `b2b_test`: 4). The rules above are therefore written
48
+ * for the environments this will meet later, not for a pair of rows anybody has
49
+ * today.
50
+ */
51
+ export class Migration20260819T155150AdminUsersFoldEmailCase extends Migration {
52
+ async up() {
53
+ // One winner per folded address: a row that already holds it first
54
+ // (`email = folded` sorts true-first under `desc`), then the earliest
55
+ // created, then the smallest id so the choice is deterministic. The winner
56
+ // is only updated when it does not already hold the folded value, which is
57
+ // also why this statement cannot violate the unique index: an update
58
+ // happens only for a group in which no row holds the target address.
59
+ this.addSql(`
60
+ with ranked as (
61
+ select "id",
62
+ lower(btrim("email")) as folded,
63
+ row_number() over (
64
+ partition by lower(btrim("email"))
65
+ order by ("email" = lower(btrim("email"))) desc,
66
+ "created_at" asc,
67
+ "id" asc
68
+ ) as rank
69
+ from "admin_users"
70
+ )
71
+ update "admin_users" au
72
+ set "email" = r.folded
73
+ from ranked r
74
+ where r."id" = au."id"
75
+ and r.rank = 1
76
+ and au."email" <> r.folded;
77
+ `);
78
+ // Say out loud what was left behind. A migration that quietly leaves an
79
+ // operator nobody can sign in as is the same class of defect as the one
80
+ // being repaired here.
81
+ this.addSql(`
82
+ do $$
83
+ declare
84
+ stranded integer;
85
+ begin
86
+ select count(*) into stranded
87
+ from "admin_users"
88
+ where "email" <> lower(btrim("email"));
89
+ if stranded > 0 then
90
+ raise warning
91
+ 'admin_users: % row(s) kept a mixed-case e-mail because another admin user already holds the folded address. They cannot be signed into until an operator renames them.',
92
+ stranded;
93
+ end if;
94
+ end $$;
95
+ `);
96
+ }
97
+ async down() {
98
+ // Deliberately empty. The original capitalisation is not recorded anywhere
99
+ // — this migration overwrites the only copy of it — so there is nothing to
100
+ // restore, and an `up` that folded a handful of rows must not be paired
101
+ // with a `down` that pretends otherwise. Rolling this back means rolling
102
+ // back the code that reads the folded form; the rows are then already in
103
+ // the shape the old code wrote, which is what it was folding to anyway.
104
+ }
105
+ }
106
+ //# sourceMappingURL=20260819T155150_admin_users_fold_email_case.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260819T155150_admin_users_fold_email_case.js","sourceRoot":"","sources":["../../src/migrations/20260819T155150_admin_users_fold_email_case.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAM,OAAO,+CAAgD,SAAQ,SAAS;IACnE,KAAK,CAAC,EAAE;QACf,mEAAmE;QACnE,sEAAsE;QACtE,2EAA2E;QAC3E,2EAA2E;QAC3E,qEAAqE;QACrE,qEAAqE;QACrE,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;KAkBX,CAAC,CAAC;QAEH,wEAAwE;QACxE,wEAAwE;QACxE,uBAAuB;QACvB,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;KAcX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,2EAA2E;QAC3E,2EAA2E;QAC3E,wEAAwE;QACxE,yEAAyE;QACzE,yEAAyE;QACzE,wEAAwE;IAC1E,CAAC;CACF"}