@solidxai/core 0.1.16 → 0.1.17-beta.1

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 (131) hide show
  1. package/CHANGELOG.md +1986 -1
  2. package/CLAUDE.md +26 -0
  3. package/dist/constants/media-file-types.d.ts +2 -2
  4. package/dist/constants/media-file-types.d.ts.map +1 -1
  5. package/dist/constants/media-file-types.js +24 -10
  6. package/dist/constants/media-file-types.js.map +1 -1
  7. package/dist/controllers/chatter-message-details.controller.d.ts.map +1 -1
  8. package/dist/controllers/chatter-message-details.controller.js +2 -6
  9. package/dist/controllers/chatter-message-details.controller.js.map +1 -1
  10. package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
  11. package/dist/controllers/chatter-message.controller.js +3 -7
  12. package/dist/controllers/chatter-message.controller.js.map +1 -1
  13. package/dist/controllers/dashboard-user-layout.controller.d.ts.map +1 -1
  14. package/dist/controllers/dashboard-user-layout.controller.js +2 -6
  15. package/dist/controllers/dashboard-user-layout.controller.js.map +1 -1
  16. package/dist/controllers/import-transaction-error-log.controller.d.ts.map +1 -1
  17. package/dist/controllers/import-transaction-error-log.controller.js +2 -6
  18. package/dist/controllers/import-transaction-error-log.controller.js.map +1 -1
  19. package/dist/controllers/import-transaction.controller.d.ts.map +1 -1
  20. package/dist/controllers/import-transaction.controller.js +2 -6
  21. package/dist/controllers/import-transaction.controller.js.map +1 -1
  22. package/dist/controllers/list-of-values.controller.d.ts.map +1 -1
  23. package/dist/controllers/list-of-values.controller.js +2 -6
  24. package/dist/controllers/list-of-values.controller.js.map +1 -1
  25. package/dist/controllers/locale.controller.d.ts.map +1 -1
  26. package/dist/controllers/locale.controller.js +2 -6
  27. package/dist/controllers/locale.controller.js.map +1 -1
  28. package/dist/controllers/media.controller.d.ts.map +1 -1
  29. package/dist/controllers/media.controller.js +9 -8
  30. package/dist/controllers/media.controller.js.map +1 -1
  31. package/dist/controllers/model-sequence.controller.d.ts.map +1 -1
  32. package/dist/controllers/model-sequence.controller.js +2 -6
  33. package/dist/controllers/model-sequence.controller.js.map +1 -1
  34. package/dist/controllers/saved-filters.controller.d.ts.map +1 -1
  35. package/dist/controllers/saved-filters.controller.js +2 -6
  36. package/dist/controllers/saved-filters.controller.js.map +1 -1
  37. package/dist/controllers/scheduled-job.controller.d.ts.map +1 -1
  38. package/dist/controllers/scheduled-job.controller.js +2 -6
  39. package/dist/controllers/scheduled-job.controller.js.map +1 -1
  40. package/dist/controllers/security-rule.controller.d.ts.map +1 -1
  41. package/dist/controllers/security-rule.controller.js +2 -6
  42. package/dist/controllers/security-rule.controller.js.map +1 -1
  43. package/dist/controllers/user-activity-history.controller.d.ts.map +1 -1
  44. package/dist/controllers/user-activity-history.controller.js +2 -6
  45. package/dist/controllers/user-activity-history.controller.js.map +1 -1
  46. package/dist/controllers/user-view-metadata.controller.d.ts.map +1 -1
  47. package/dist/controllers/user-view-metadata.controller.js +2 -6
  48. package/dist/controllers/user-view-metadata.controller.js.map +1 -1
  49. package/dist/controllers/workflow-definition.controller.d.ts.map +1 -1
  50. package/dist/controllers/workflow-definition.controller.js +2 -6
  51. package/dist/controllers/workflow-definition.controller.js.map +1 -1
  52. package/dist/controllers/workflow-execution-artifact.controller.d.ts.map +1 -1
  53. package/dist/controllers/workflow-execution-artifact.controller.js +2 -6
  54. package/dist/controllers/workflow-execution-artifact.controller.js.map +1 -1
  55. package/dist/controllers/workflow-execution-log.controller.d.ts.map +1 -1
  56. package/dist/controllers/workflow-execution-log.controller.js +2 -6
  57. package/dist/controllers/workflow-execution-log.controller.js.map +1 -1
  58. package/dist/controllers/workflow-execution.controller.d.ts.map +1 -1
  59. package/dist/controllers/workflow-execution.controller.js +2 -6
  60. package/dist/controllers/workflow-execution.controller.js.map +1 -1
  61. package/dist/controllers/workflow-secret.controller.d.ts.map +1 -1
  62. package/dist/controllers/workflow-secret.controller.js +2 -6
  63. package/dist/controllers/workflow-secret.controller.js.map +1 -1
  64. package/dist/controllers/workflow-step-execution.controller.d.ts.map +1 -1
  65. package/dist/controllers/workflow-step-execution.controller.js +2 -6
  66. package/dist/controllers/workflow-step-execution.controller.js.map +1 -1
  67. package/dist/controllers/workflow-trigger-execution.controller.d.ts.map +1 -1
  68. package/dist/controllers/workflow-trigger-execution.controller.js +2 -6
  69. package/dist/controllers/workflow-trigger-execution.controller.js.map +1 -1
  70. package/dist/dtos/basic-filters.dto.d.ts +3 -5
  71. package/dist/dtos/basic-filters.dto.d.ts.map +1 -1
  72. package/dist/dtos/basic-filters.dto.js +6 -8
  73. package/dist/dtos/basic-filters.dto.js.map +1 -1
  74. package/dist/dtos/otp-sign-up.dto.d.ts +1 -0
  75. package/dist/dtos/otp-sign-up.dto.d.ts.map +1 -1
  76. package/dist/dtos/otp-sign-up.dto.js +12 -1
  77. package/dist/dtos/otp-sign-up.dto.js.map +1 -1
  78. package/dist/enums/show-soft-deleted.enum.d.ts +5 -0
  79. package/dist/enums/show-soft-deleted.enum.d.ts.map +1 -0
  80. package/dist/enums/show-soft-deleted.enum.js +9 -0
  81. package/dist/enums/show-soft-deleted.enum.js.map +1 -0
  82. package/dist/index.d.ts +1 -0
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +1 -0
  85. package/dist/index.js.map +1 -1
  86. package/dist/services/authentication.service.d.ts.map +1 -1
  87. package/dist/services/authentication.service.js +15 -6
  88. package/dist/services/authentication.service.js.map +1 -1
  89. package/dist/services/chatter-message.service.d.ts +2 -0
  90. package/dist/services/chatter-message.service.d.ts.map +1 -1
  91. package/dist/services/chatter-message.service.js +48 -21
  92. package/dist/services/chatter-message.service.js.map +1 -1
  93. package/dist/services/settings/default-settings-provider.service.d.ts +20 -0
  94. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  95. package/dist/services/settings/default-settings-provider.service.js +12 -0
  96. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  97. package/dist/solid-core.module.d.ts.map +1 -1
  98. package/dist/solid-core.module.js +5 -1
  99. package/dist/solid-core.module.js.map +1 -1
  100. package/package.json +1 -1
  101. package/postman/signup-intent-verification.postman_collection.json +573 -0
  102. package/src/constants/media-file-types.ts +76 -24
  103. package/src/controllers/chatter-message-details.controller.ts +1 -4
  104. package/src/controllers/chatter-message.controller.ts +1 -4
  105. package/src/controllers/dashboard-user-layout.controller.ts +1 -4
  106. package/src/controllers/import-transaction-error-log.controller.ts +1 -4
  107. package/src/controllers/import-transaction.controller.ts +1 -4
  108. package/src/controllers/list-of-values.controller.ts +1 -4
  109. package/src/controllers/locale.controller.ts +1 -4
  110. package/src/controllers/media.controller.ts +19 -7
  111. package/src/controllers/model-sequence.controller.ts +1 -4
  112. package/src/controllers/saved-filters.controller.ts +1 -4
  113. package/src/controllers/scheduled-job.controller.ts +1 -4
  114. package/src/controllers/security-rule.controller.ts +1 -4
  115. package/src/controllers/user-activity-history.controller.ts +1 -4
  116. package/src/controllers/user-view-metadata.controller.ts +1 -4
  117. package/src/controllers/workflow-definition.controller.ts +1 -4
  118. package/src/controllers/workflow-execution-artifact.controller.ts +1 -4
  119. package/src/controllers/workflow-execution-log.controller.ts +1 -4
  120. package/src/controllers/workflow-execution.controller.ts +1 -4
  121. package/src/controllers/workflow-secret.controller.ts +1 -4
  122. package/src/controllers/workflow-step-execution.controller.ts +1 -4
  123. package/src/controllers/workflow-trigger-execution.controller.ts +1 -4
  124. package/src/dtos/basic-filters.dto.ts +6 -7
  125. package/src/dtos/otp-sign-up.dto.ts +11 -1
  126. package/src/enums/show-soft-deleted.enum.ts +4 -0
  127. package/src/index.ts +1 -0
  128. package/src/services/authentication.service.ts +61 -26
  129. package/src/services/chatter-message.service.ts +54 -23
  130. package/src/services/settings/default-settings-provider.service.ts +12 -0
  131. package/src/solid-core.module.ts +12 -2
@@ -0,0 +1,573 @@
1
+ {
2
+ "info": {
3
+ "name": "SolidX — SignupIntent verification",
4
+ "description": "Verifies commits 1-5 of the extension-user signup work: cross-type uniqueness, allowPublicRegistration enforcement, SignupIntent dispatch, per-entry seed flags, and OTP provider wiring.\n\nRUN AGAINST an app that registers an IExtensionUserCreationProvider (e.g. solid-library-management, which has LmUserCreationProvider). Several checks are meaningless without one.\n\nIMPORTANT — what this collection can and cannot see:\nThe STI discriminator column `type` is declared only in @TableInheritance, never as a class property, so it is NOT serialized into any response. This collection therefore uses the presence of an extension-only field (`userType`) as a PROXY for \"this row is the child entity\": class-transformer emits `userType` (even as null) for an LmUser and omits it entirely for a base User.\n\nThat proxy is good enough to catch a wrong dispatch, but the definitive check is SQL. Each folder's description carries the query to run.\n\nSet `childModelPath`, `childTypeName` and `userTypeValue` to match your app before running.",
5
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
6
+ },
7
+ "variable": [
8
+ { "key": "baseUrl", "value": "http://localhost:3000/api", "type": "string" },
9
+ { "key": "adminUsername", "value": "sa", "type": "string" },
10
+ { "key": "adminPassword", "value": "Admin@3214$", "type": "string" },
11
+ { "key": "childModelPath", "value": "lm-user", "type": "string" },
12
+ { "key": "childTypeName", "value": "LmUser", "type": "string" },
13
+ { "key": "userTypeValue", "value": "Member", "type": "string" },
14
+ { "key": "defaultRole", "value": "Public", "type": "string" },
15
+ { "key": "privilegedRole", "value": "Admin", "type": "string" },
16
+ { "key": "testPassword", "value": "Test@1234", "type": "string" },
17
+ { "key": "adminToken", "value": "", "type": "string" },
18
+ { "key": "otp", "value": "", "type": "string" }
19
+ ],
20
+ "item": [
21
+ {
22
+ "name": "0 — Setup",
23
+ "description": "Authenticates as the seeded system admin and stores the JWT for the folders that need one.",
24
+ "item": [
25
+ {
26
+ "name": "Login as sa",
27
+ "request": {
28
+ "method": "POST",
29
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
30
+ "url": { "raw": "{{baseUrl}}/iam/authenticate", "host": ["{{baseUrl}}"], "path": ["iam", "authenticate"] },
31
+ "body": {
32
+ "mode": "raw",
33
+ "raw": "{\n \"username\": \"{{adminUsername}}\",\n \"password\": \"{{adminPassword}}\"\n}"
34
+ }
35
+ },
36
+ "event": [
37
+ {
38
+ "listen": "test",
39
+ "script": {
40
+ "exec": [
41
+ "pm.test('sa can log in', () => pm.response.code === 200);",
42
+ "const d = pm.response.json().data;",
43
+ "pm.collectionVariables.set('adminToken', d.accessToken);",
44
+ "pm.test('returned an access token', () => pm.expect(d.accessToken).to.be.a('string').and.not.empty);",
45
+ "// Commit 4 keeps sa a base User: it is seeded without isExtensionUser.",
46
+ "pm.test('sa still has its roles', () => pm.expect(d.user.roles).to.be.an('array').that.is.not.empty);"
47
+ ]
48
+ }
49
+ }
50
+ ]
51
+ }
52
+ ]
53
+ },
54
+
55
+ {
56
+ "name": "1 — Public registration (SelfRegistration)",
57
+ "description": "POST /iam/register.\n\nExpectation: creates the app's EXTENSION entity, always takes the configured defaultRole, and ignores any roles in the body.\n\nSQL:\n select id, username, type, user_type from ss_user order by id desc limit 5;\n -- every row created by this folder must have type = '<childTypeName>'",
58
+ "item": [
59
+ {
60
+ "name": "1.1 core fields only (what the stock signup screen sends)",
61
+ "request": {
62
+ "method": "POST",
63
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
64
+ "url": { "raw": "{{baseUrl}}/iam/register", "host": ["{{baseUrl}}"], "path": ["iam", "register"] },
65
+ "body": {
66
+ "mode": "raw",
67
+ "raw": "{\n \"username\": \"pub_core_{{$timestamp}}\",\n \"email\": \"pub_core_{{$timestamp}}@example.com\",\n \"password\": \"{{testPassword}}\"\n}"
68
+ }
69
+ },
70
+ "event": [
71
+ {
72
+ "listen": "test",
73
+ "script": {
74
+ "exec": [
75
+ "// THE case that is broken before this work: a core-fields-only body used to",
76
+ "// create a base User because nothing in it looked like an extension field.",
77
+ "pm.test('created (not 400, not 500)', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
78
+ "const d = pm.response.json().data;",
79
+ "pm.test('is the extension entity (userType key present)', () => pm.expect(d).to.have.property('userType'));",
80
+ "// Step 7 obligation: the provider must default the discriminator, because the",
81
+ "// stock signup screen cannot send one and nothing in core will catch a null.",
82
+ "pm.test('provider defaulted the discriminator', () => pm.expect(d.userType, 'null userType — buildExtensionEntity is not defaulting it').to.not.be.null);"
83
+ ]
84
+ }
85
+ }
86
+ ]
87
+ },
88
+ {
89
+ "name": "1.2 with an explicit userType",
90
+ "request": {
91
+ "method": "POST",
92
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
93
+ "url": { "raw": "{{baseUrl}}/iam/register", "host": ["{{baseUrl}}"], "path": ["iam", "register"] },
94
+ "body": {
95
+ "mode": "raw",
96
+ "raw": "{\n \"username\": \"pub_typed_{{$timestamp}}\",\n \"email\": \"pub_typed_{{$timestamp}}@example.com\",\n \"password\": \"{{testPassword}}\",\n \"userType\": \"{{userTypeValue}}\"\n}"
97
+ }
98
+ },
99
+ "event": [
100
+ {
101
+ "listen": "test",
102
+ "script": {
103
+ "exec": [
104
+ "pm.test('created', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
105
+ "const d = pm.response.json().data;",
106
+ "pm.test('extension field round-trips', () => pm.expect(d.userType).to.eql(pm.collectionVariables.get('userTypeValue')));"
107
+ ]
108
+ }
109
+ }
110
+ ]
111
+ },
112
+ {
113
+ "name": "1.3a ESCALATION — register asking for a privileged role",
114
+ "request": {
115
+ "method": "POST",
116
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
117
+ "url": { "raw": "{{baseUrl}}/iam/register", "host": ["{{baseUrl}}"], "path": ["iam", "register"] },
118
+ "body": {
119
+ "mode": "raw",
120
+ "raw": "{\n \"username\": \"esc_{{$timestamp}}\",\n \"email\": \"esc_{{$timestamp}}@example.com\",\n \"password\": \"{{testPassword}}\",\n \"roles\": [\"{{privilegedRole}}\"]\n}"
121
+ }
122
+ },
123
+ "event": [
124
+ {
125
+ "listen": "prerequest",
126
+ "script": {
127
+ "exec": [
128
+ "// Remember the identity so 1.3b can log in as it.",
129
+ "const u = 'esc_' + Math.floor(Date.now() / 1000);",
130
+ "pm.collectionVariables.set('escUsername', u);",
131
+ "pm.collectionVariables.set('escEmail', u + '@example.com');"
132
+ ]
133
+ }
134
+ },
135
+ {
136
+ "listen": "test",
137
+ "script": {
138
+ "exec": [
139
+ "// Before this work an anonymous caller got whatever roles they asked for:",
140
+ "// the only check was that the role EXISTS, never that the caller may hold it.",
141
+ "pm.test('request succeeds (roles are ignored, not rejected)', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
142
+ "pm.test('server logged a warning about ignoring roles — check app logs', () => true);",
143
+ "// The real assertion is 1.3c: the response body does not list roles."
144
+ ]
145
+ }
146
+ }
147
+ ]
148
+ },
149
+ {
150
+ "name": "1.3b login as the escalation-attempt user",
151
+ "request": {
152
+ "method": "POST",
153
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
154
+ "url": { "raw": "{{baseUrl}}/iam/authenticate", "host": ["{{baseUrl}}"], "path": ["iam", "authenticate"] },
155
+ "body": {
156
+ "mode": "raw",
157
+ "raw": "{\n \"username\": \"{{escUsername}}\",\n \"password\": \"{{testPassword}}\"\n}"
158
+ }
159
+ },
160
+ "event": [
161
+ {
162
+ "listen": "test",
163
+ "script": {
164
+ "exec": [
165
+ "pm.test('can log in', () => pm.response.code === 200);",
166
+ "const d = pm.response.json().data;",
167
+ "pm.collectionVariables.set('escToken', d.accessToken);",
168
+ "const roles = d.user.roles || [];",
169
+ "const privileged = pm.collectionVariables.get('privilegedRole');",
170
+ "pm.test('DID NOT get the role it asked for', () => pm.expect(roles).to.not.include(privileged));",
171
+ "pm.test('got the configured default role instead', () => pm.expect(roles).to.include(pm.collectionVariables.get('defaultRole')));"
172
+ ]
173
+ }
174
+ }
175
+ ]
176
+ },
177
+ {
178
+ "name": "1.4 stray extra key is ignored (used to 500 with no provider)",
179
+ "request": {
180
+ "method": "POST",
181
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
182
+ "url": { "raw": "{{baseUrl}}/iam/register", "host": ["{{baseUrl}}"], "path": ["iam", "register"] },
183
+ "body": {
184
+ "mode": "raw",
185
+ "raw": "{\n \"username\": \"stray_{{$timestamp}}\",\n \"email\": \"stray_{{$timestamp}}@example.com\",\n \"password\": \"{{testPassword}}\",\n \"passwordConfirm\": \"{{testPassword}}\",\n \"emial\": \"typo@example.com\"\n}"
186
+ }
187
+ },
188
+ "event": [
189
+ {
190
+ "listen": "test",
191
+ "script": {
192
+ "exec": [
193
+ "// On an app with NO provider this used to throw 500 'No ExtensionUserCreationProvider",
194
+ "// registered' from a @Public() endpoint, because any unknown key tripped the sniff.",
195
+ "pm.test('no 500', () => pm.expect(pm.response.code).to.not.eql(500));",
196
+ "pm.test('created', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));"
197
+ ]
198
+ }
199
+ }
200
+ ]
201
+ }
202
+ ]
203
+ },
204
+
205
+ {
206
+ "name": "2 — Admin console (CoreUser)",
207
+ "description": "POST /iam/register-private — where solid-core-ui's CreateUser.tsx posts.\n\nExpectation: creates a BASE User with the roles the admin chose. Must match pre-change behaviour exactly.\n\nSQL:\n select id, username, type from ss_user where username like 'adm_%' order by id desc limit 5;\n -- must be type = 'User'",
208
+ "item": [
209
+ {
210
+ "name": "2.1 admin creates a user",
211
+ "request": {
212
+ "method": "POST",
213
+ "header": [
214
+ { "key": "Content-Type", "value": "application/json" },
215
+ { "key": "Authorization", "value": "Bearer {{adminToken}}" }
216
+ ],
217
+ "url": { "raw": "{{baseUrl}}/iam/register-private", "host": ["{{baseUrl}}"], "path": ["iam", "register-private"] },
218
+ "body": {
219
+ "mode": "raw",
220
+ "raw": "{\n \"username\": \"{{admUsername}}\",\n \"email\": \"{{admEmail}}\",\n \"password\": \"{{testPassword}}\",\n \"fullName\": \"Admin Created\",\n \"roles\": [\"{{privilegedRole}}\"],\n \"isAllowedToGenerateApiKeys\": true\n}"
221
+ }
222
+ },
223
+ "event": [
224
+ {
225
+ "listen": "prerequest",
226
+ "script": {
227
+ "exec": [
228
+ "const u = 'adm_' + Math.floor(Date.now() / 1000);",
229
+ "pm.collectionVariables.set('admUsername', u);",
230
+ "pm.collectionVariables.set('admEmail', u + '@example.com');"
231
+ ]
232
+ }
233
+ },
234
+ {
235
+ "listen": "test",
236
+ "script": {
237
+ "exec": [
238
+ "pm.test('created', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
239
+ "const d = pm.response.json().data;",
240
+ "// CoreUser intent: the provider is never consulted, so no extension fields.",
241
+ "pm.test('is a base User (no extension fields)', () => pm.expect(d).to.not.have.property('userType'));",
242
+ "// Guards the preferEntityApiKeyFlag removal: an explicit DTO value must win.",
243
+ "pm.test('honoured isAllowedToGenerateApiKeys', () => pm.expect(d.isAllowedToGenerateApiKeys).to.eql(true));"
244
+ ]
245
+ }
246
+ }
247
+ ]
248
+ },
249
+ {
250
+ "name": "2.2 login as that user — admin-chosen roles were kept",
251
+ "request": {
252
+ "method": "POST",
253
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
254
+ "url": { "raw": "{{baseUrl}}/iam/authenticate", "host": ["{{baseUrl}}"], "path": ["iam", "authenticate"] },
255
+ "body": {
256
+ "mode": "raw",
257
+ "raw": "{\n \"username\": \"{{admUsername}}\",\n \"password\": \"{{testPassword}}\"\n}"
258
+ }
259
+ },
260
+ "event": [
261
+ {
262
+ "listen": "test",
263
+ "script": {
264
+ "exec": [
265
+ "pm.test('can log in', () => pm.response.code === 200);",
266
+ "const roles = pm.response.json().data.user.roles || [];",
267
+ "// CoreUser trusts caller-supplied roles — unlike public registration.",
268
+ "pm.test('kept the role the admin chose', () => pm.expect(roles).to.include(pm.collectionVariables.get('privilegedRole')));"
269
+ ]
270
+ }
271
+ }
272
+ ]
273
+ }
274
+ ]
275
+ },
276
+
277
+ {
278
+ "name": "3 — Extension model CRUD (ExtensionModel)",
279
+ "description": "POST /{{childModelPath}} — the generated CRUD form for the child model.\n\nExpectation: creates the extension entity, and roles come from provider.roles(dto) unconditionally. This is the ONLY path that still calls roles(), so it is the only one where a provider requiring userType can reject.\n\nSQL:\n select id, username, type, user_type from ss_user where type = '<childTypeName>' order by id desc limit 5;",
280
+ "item": [
281
+ {
282
+ "name": "3.1 create with userType",
283
+ "request": {
284
+ "method": "POST",
285
+ "header": [
286
+ { "key": "Content-Type", "value": "application/json" },
287
+ { "key": "Authorization", "value": "Bearer {{adminToken}}" }
288
+ ],
289
+ "url": { "raw": "{{baseUrl}}/{{childModelPath}}", "host": ["{{baseUrl}}"], "path": ["{{childModelPath}}"] },
290
+ "body": {
291
+ "mode": "raw",
292
+ "raw": "{\n \"username\": \"crud_{{$timestamp}}\",\n \"email\": \"crud_{{$timestamp}}@example.com\",\n \"password\": \"{{testPassword}}\",\n \"userType\": \"{{userTypeValue}}\"\n}"
293
+ }
294
+ },
295
+ "event": [
296
+ {
297
+ "listen": "test",
298
+ "script": {
299
+ "exec": [
300
+ "pm.test('created', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
301
+ "const d = pm.response.json().data;",
302
+ "pm.test('extension field set', () => pm.expect(d.userType).to.eql(pm.collectionVariables.get('userTypeValue')));"
303
+ ]
304
+ }
305
+ }
306
+ ]
307
+ },
308
+ {
309
+ "name": "3.2 create WITHOUT userType — provider.roles() should reject",
310
+ "request": {
311
+ "method": "POST",
312
+ "header": [
313
+ { "key": "Content-Type", "value": "application/json" },
314
+ { "key": "Authorization", "value": "Bearer {{adminToken}}" }
315
+ ],
316
+ "url": { "raw": "{{baseUrl}}/{{childModelPath}}", "host": ["{{baseUrl}}"], "path": ["{{childModelPath}}"] },
317
+ "body": {
318
+ "mode": "raw",
319
+ "raw": "{\n \"username\": \"crud_nt_{{$timestamp}}\",\n \"email\": \"crud_nt_{{$timestamp}}@example.com\",\n \"password\": \"{{testPassword}}\"\n}"
320
+ }
321
+ },
322
+ "event": [
323
+ {
324
+ "listen": "test",
325
+ "script": {
326
+ "exec": [
327
+ "// Only meaningful if your provider's roles() throws on a missing discriminator",
328
+ "// (the LmUser/InstituteUser shape does). If yours returns [] instead, expect 201",
329
+ "// and the configured defaultRole — adjust this test to match your provider.",
330
+ "pm.test('rejected, or created with the default role', () => pm.expect(pm.response.code).to.be.oneOf([400, 200, 201]));",
331
+ "if (pm.response.code === 400) {",
332
+ " pm.test('rejection came from the provider', () => pm.expect(JSON.stringify(pm.response.json())).to.match(/userType|role/i));",
333
+ "}"
334
+ ]
335
+ }
336
+ }
337
+ ]
338
+ }
339
+ ]
340
+ },
341
+
342
+ {
343
+ "name": "4 — Cross-type uniqueness (commit 1)",
344
+ "description": "Creates a BASE User via register-private, then tries to register the SAME email publicly, which would create the CHILD type.\n\nBefore commit 1 the uniqueness check ran against the child repository, which TypeORM scopes to one discriminator — and email/mobile carry non-unique @Index(), so no DB constraint would have caught it. The duplicate was silently accepted.",
345
+ "item": [
346
+ {
347
+ "name": "4.1 create a base User with a known email",
348
+ "request": {
349
+ "method": "POST",
350
+ "header": [
351
+ { "key": "Content-Type", "value": "application/json" },
352
+ { "key": "Authorization", "value": "Bearer {{adminToken}}" }
353
+ ],
354
+ "url": { "raw": "{{baseUrl}}/iam/register-private", "host": ["{{baseUrl}}"], "path": ["iam", "register-private"] },
355
+ "body": {
356
+ "mode": "raw",
357
+ "raw": "{\n \"username\": \"{{dupUsername}}\",\n \"email\": \"{{dupEmail}}\",\n \"password\": \"{{testPassword}}\",\n \"roles\": [\"{{defaultRole}}\"]\n}"
358
+ }
359
+ },
360
+ "event": [
361
+ {
362
+ "listen": "prerequest",
363
+ "script": {
364
+ "exec": [
365
+ "const u = 'dup_' + Math.floor(Date.now() / 1000);",
366
+ "pm.collectionVariables.set('dupUsername', u);",
367
+ "pm.collectionVariables.set('dupEmail', u + '@example.com');"
368
+ ]
369
+ }
370
+ },
371
+ {
372
+ "listen": "test",
373
+ "script": { "exec": ["pm.test('base user created', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));"] }
374
+ }
375
+ ]
376
+ },
377
+ {
378
+ "name": "4.2 public register with the SAME email — must 409",
379
+ "request": {
380
+ "method": "POST",
381
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
382
+ "url": { "raw": "{{baseUrl}}/iam/register", "host": ["{{baseUrl}}"], "path": ["iam", "register"] },
383
+ "body": {
384
+ "mode": "raw",
385
+ "raw": "{\n \"username\": \"{{dupUsername}}_other\",\n \"email\": \"{{dupEmail}}\",\n \"password\": \"{{testPassword}}\"\n}"
386
+ }
387
+ },
388
+ "event": [
389
+ {
390
+ "listen": "test",
391
+ "script": {
392
+ "exec": [
393
+ "pm.test('duplicate email across types is rejected', () => pm.expect(pm.response.code).to.eql(409));",
394
+ "pm.test('message names the email', () => pm.expect(pm.response.text()).to.match(/email/i));"
395
+ ]
396
+ }
397
+ }
398
+ ]
399
+ }
400
+ ]
401
+ },
402
+
403
+ {
404
+ "name": "5 — Passwordless OTP registration (commit 5)",
405
+ "description": "Requires setting `passwordLessAuth` on. otpInitiateRegistration used to do `new User()` and save through the base repository, never consulting the registry.\n\n5.2 needs the OTP: read it from the app log / SMS-WhatsApp stub / DB and set the `otp` collection variable before running it.\n\nSQL:\n select id, username, type, active, mobile_verification_token_on_registration from ss_user where username like 'otp_%' order by id desc limit 3;\n -- after initiate: type = '<childTypeName>', active = false\n -- after confirm: active = true, type UNCHANGED (the STI round-trip check)",
406
+ "item": [
407
+ {
408
+ "name": "5.1 initiate",
409
+ "request": {
410
+ "method": "POST",
411
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
412
+ "url": { "raw": "{{baseUrl}}/iam/otp/register/initiate", "host": ["{{baseUrl}}"], "path": ["iam", "otp", "register", "initiate"] },
413
+ "body": {
414
+ "mode": "raw",
415
+ "raw": "{\n \"username\": \"{{otpUsername}}\",\n \"email\": \"{{otpEmail}}\",\n \"mobile\": \"{{otpMobile}}\"\n}"
416
+ }
417
+ },
418
+ "event": [
419
+ {
420
+ "listen": "prerequest",
421
+ "script": {
422
+ "exec": [
423
+ "const t = Math.floor(Date.now() / 1000);",
424
+ "pm.collectionVariables.set('otpUsername', 'otp_' + t);",
425
+ "pm.collectionVariables.set('otpEmail', 'otp_' + t + '@example.com');",
426
+ "pm.collectionVariables.set('otpMobile', '9' + String(t).slice(-9));"
427
+ ]
428
+ }
429
+ },
430
+ {
431
+ "listen": "test",
432
+ "script": {
433
+ "exec": [
434
+ "// 400 PASSWORDLESS_REGISTRATION_DISABLED means passwordLessAuth is off — enable it.",
435
+ "// 403 means allowPublicRegistration is off — folder 6 turned it off, turn it back on.",
436
+ "pm.test('OTP sent', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
437
+ "if (pm.response.code === 400) { console.warn('passwordLessAuth appears to be disabled'); }",
438
+ "console.log('Now read the OTP and set the `otp` collection variable, then run 5.2');"
439
+ ]
440
+ }
441
+ }
442
+ ]
443
+ },
444
+ {
445
+ "name": "5.2 confirm (set `otp` first)",
446
+ "request": {
447
+ "method": "POST",
448
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
449
+ "url": { "raw": "{{baseUrl}}/iam/otp/register/confirm", "host": ["{{baseUrl}}"], "path": ["iam", "otp", "register", "confirm"] },
450
+ "body": {
451
+ "mode": "raw",
452
+ "raw": "{\n \"type\": \"mobile\",\n \"identifier\": \"{{otpMobile}}\",\n \"otp\": \"{{otp}}\"\n}"
453
+ }
454
+ },
455
+ "event": [
456
+ {
457
+ "listen": "test",
458
+ "script": {
459
+ "exec": [
460
+ "pm.test('confirmed', () => pm.expect(pm.response.code).to.eql(200));",
461
+ "const d = pm.response.json().data;",
462
+ "pm.test('user activated', () => pm.expect(d.active).to.eql(true));",
463
+ "// The row was loaded through the BASE repository and saved back. Confirm in SQL that",
464
+ "// `type` is still the child type — that is the STI round-trip assumption in commit 5."
465
+ ]
466
+ }
467
+ }
468
+ ]
469
+ }
470
+ ]
471
+ },
472
+
473
+ {
474
+ "name": "6 — allowPublicRegistration = false (commit 2 + 3)",
475
+ "description": "MANUAL STEP FIRST: set the `allowPublicRegistration` setting to false (Settings screen, or IAM_ALLOW_PUBLIC_REGISTRATION=false and restart).\n\nThe setting was dead config before this work — defined, admin-editable, read by nothing.\n\nThe scoping matters more than the guard: only the two anonymous doors may close. If 6.3 or 6.4 fail, disabling public registration has taken down user creation across the system, which is far worse than the bug being fixed.\n\nRemember to turn it back on afterwards.",
476
+ "item": [
477
+ {
478
+ "name": "6.1 public register — must 403",
479
+ "request": {
480
+ "method": "POST",
481
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
482
+ "url": { "raw": "{{baseUrl}}/iam/register", "host": ["{{baseUrl}}"], "path": ["iam", "register"] },
483
+ "body": {
484
+ "mode": "raw",
485
+ "raw": "{\n \"username\": \"blocked_{{$timestamp}}\",\n \"email\": \"blocked_{{$timestamp}}@example.com\",\n \"password\": \"{{testPassword}}\"\n}"
486
+ }
487
+ },
488
+ "event": [
489
+ {
490
+ "listen": "test",
491
+ "script": {
492
+ "exec": [
493
+ "pm.test('public registration is blocked', () => pm.expect(pm.response.code).to.eql(403));",
494
+ "// A 201 here means the setting is still not being read — check that getConfigValue",
495
+ "// returns the string 'false' rather than boolean false, which a falsy check misses."
496
+ ]
497
+ }
498
+ }
499
+ ]
500
+ },
501
+ {
502
+ "name": "6.2 OTP initiate — must 403",
503
+ "request": {
504
+ "method": "POST",
505
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
506
+ "url": { "raw": "{{baseUrl}}/iam/otp/register/initiate", "host": ["{{baseUrl}}"], "path": ["iam", "otp", "register", "initiate"] },
507
+ "body": {
508
+ "mode": "raw",
509
+ "raw": "{\n \"username\": \"blocked_otp_{{$timestamp}}\",\n \"email\": \"blocked_otp_{{$timestamp}}@example.com\",\n \"mobile\": \"9000000001\"\n}"
510
+ }
511
+ },
512
+ "event": [
513
+ {
514
+ "listen": "test",
515
+ "script": { "exec": ["pm.test('OTP registration is blocked', () => pm.expect(pm.response.code).to.eql(403));"] }
516
+ }
517
+ ]
518
+ },
519
+ {
520
+ "name": "6.3 SCOPING — admin console must still work",
521
+ "request": {
522
+ "method": "POST",
523
+ "header": [
524
+ { "key": "Content-Type", "value": "application/json" },
525
+ { "key": "Authorization", "value": "Bearer {{adminToken}}" }
526
+ ],
527
+ "url": { "raw": "{{baseUrl}}/iam/register-private", "host": ["{{baseUrl}}"], "path": ["iam", "register-private"] },
528
+ "body": {
529
+ "mode": "raw",
530
+ "raw": "{\n \"username\": \"scoped_adm_{{$timestamp}}\",\n \"email\": \"scoped_adm_{{$timestamp}}@example.com\",\n \"password\": \"{{testPassword}}\",\n \"roles\": [\"{{defaultRole}}\"]\n}"
531
+ }
532
+ },
533
+ "event": [
534
+ {
535
+ "listen": "test",
536
+ "script": {
537
+ "exec": [
538
+ "pm.test('admin can still create users', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
539
+ "// A 403 here means the guard is not scoped to SelfRegistration."
540
+ ]
541
+ }
542
+ }
543
+ ]
544
+ },
545
+ {
546
+ "name": "6.4 SCOPING — extension model CRUD must still work",
547
+ "request": {
548
+ "method": "POST",
549
+ "header": [
550
+ { "key": "Content-Type", "value": "application/json" },
551
+ { "key": "Authorization", "value": "Bearer {{adminToken}}" }
552
+ ],
553
+ "url": { "raw": "{{baseUrl}}/{{childModelPath}}", "host": ["{{baseUrl}}"], "path": ["{{childModelPath}}"] },
554
+ "body": {
555
+ "mode": "raw",
556
+ "raw": "{\n \"username\": \"scoped_crud_{{$timestamp}}\",\n \"email\": \"scoped_crud_{{$timestamp}}@example.com\",\n \"password\": \"{{testPassword}}\",\n \"userType\": \"{{userTypeValue}}\"\n}"
557
+ }
558
+ },
559
+ "event": [
560
+ {
561
+ "listen": "test",
562
+ "script": {
563
+ "exec": [
564
+ "pm.test('extension model CRUD still works', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));"
565
+ ]
566
+ }
567
+ }
568
+ ]
569
+ }
570
+ ]
571
+ }
572
+ ]
573
+ }