@solidxai/core 0.1.16-beta.1 → 0.1.16-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1316 -0
- package/CLAUDE.md +26 -0
- package/dist/controllers/authentication.controller.d.ts.map +1 -1
- package/dist/controllers/authentication.controller.js +3 -2
- package/dist/controllers/authentication.controller.js.map +1 -1
- package/dist/dtos/otp-sign-up.dto.d.ts +1 -0
- package/dist/dtos/otp-sign-up.dto.d.ts.map +1 -1
- package/dist/dtos/otp-sign-up.dto.js +12 -1
- package/dist/dtos/otp-sign-up.dto.js.map +1 -1
- package/dist/enums/signup-intent.enum.d.ts +6 -0
- package/dist/enums/signup-intent.enum.d.ts.map +1 -0
- package/dist/enums/signup-intent.enum.js +10 -0
- package/dist/enums/signup-intent.enum.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +4 -1
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/module-test-data.service.d.ts.map +1 -1
- package/dist/seeders/module-test-data.service.js +3 -1
- package/dist/seeders/module-test-data.service.js.map +1 -1
- package/dist/services/authentication.service.d.ts +4 -2
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +83 -46
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/crud.service.d.ts.map +1 -1
- package/dist/services/crud.service.js +2 -1
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/user.service.d.ts +4 -0
- package/dist/services/user.service.d.ts.map +1 -1
- package/dist/services/user.service.js +15 -0
- package/dist/services/user.service.js.map +1 -1
- package/dist/testing/contracts/testing-metadata.types.d.ts +1 -0
- package/dist/testing/contracts/testing-metadata.types.d.ts.map +1 -1
- package/dist/testing/contracts/testing-metadata.types.js.map +1 -1
- package/package.json +1 -1
- package/postman/signup-intent-verification.postman_collection.json +573 -0
- package/postman/signup-issues +3 -0
- package/src/controllers/authentication.controller.ts +5 -2
- package/src/dtos/otp-sign-up.dto.ts +11 -1
- package/src/enums/signup-intent.enum.ts +22 -0
- package/src/index.ts +1 -0
- package/src/seeders/module-metadata-seeder.service.ts +38 -5
- package/src/seeders/module-test-data.service.ts +9 -1
- package/src/services/authentication.service.ts +209 -63
- package/src/services/crud.service.ts +2 -1
- package/src/services/user.service.ts +34 -0
- package/src/testing/contracts/testing-metadata.types.ts +12 -0
- package/postman/instant-logout.postman_collection.json +0 -1493
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declares what kind of user a signup call is creating.
|
|
3
|
+
*
|
|
4
|
+
* The distinction is a domain one, not a mechanical one:
|
|
5
|
+
*
|
|
6
|
+
* - a base `User` is someone who administers the system - the seeded `sa`, users
|
|
7
|
+
* created from the admin console;
|
|
8
|
+
* - an extension user is someone who *uses* the app - public signup, OTP
|
|
9
|
+
* registration, OAuth sign-in, and the extension model's own CRUD form.
|
|
10
|
+
*
|
|
11
|
+
* Each call site states which it means. Nothing inspects the request body to
|
|
12
|
+
* decide, so the same payload posted to two endpoints cannot produce two
|
|
13
|
+
* different kinds of user.
|
|
14
|
+
*/
|
|
15
|
+
export enum SignupIntent {
|
|
16
|
+
/** Public self-registration: an anonymous visitor creating their own account. */
|
|
17
|
+
SelfRegistration = 'self-registration',
|
|
18
|
+
/** Explicit create of the extension model, via its generated CRUD form. */
|
|
19
|
+
ExtensionModel = 'extension-model',
|
|
20
|
+
/** Admin console and internal callers: a plain core user. */
|
|
21
|
+
CoreUser = 'core-user',
|
|
22
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -193,6 +193,7 @@ export * from './entities/workflow-secret.entity'
|
|
|
193
193
|
|
|
194
194
|
export * from './enums/auth-type.enum'
|
|
195
195
|
export * from './enums/legacy-table-type.enum'
|
|
196
|
+
export * from './enums/signup-intent.enum'
|
|
196
197
|
export * from './decorators/disallow-in-production.decorator'
|
|
197
198
|
|
|
198
199
|
export * from './filters/http-exception.filter'
|
|
@@ -41,6 +41,7 @@ import { User } from '../entities/user.entity';
|
|
|
41
41
|
import { MENU_ROLE_JOIN_TABLE_NAME, MENU_ROLE_JOIN_TABLE_NAME_MENU_COL, MENU_ROLE_JOIN_TABLE_NAME_ROLE_COL } from '../dtos/create-menu-item-metadata.dto';
|
|
42
42
|
import { DEFAULT_SA_PASSWORD } from '../dtos/create-user.dto';
|
|
43
43
|
import { SignUpDto } from '../dtos/sign-up.dto';
|
|
44
|
+
import { SignupIntent } from '../enums/signup-intent.enum';
|
|
44
45
|
import {
|
|
45
46
|
ADMIN_ROLE_NAME,
|
|
46
47
|
ALLOWED_TO_EXPORT_ROLE_NAME,
|
|
@@ -86,6 +87,33 @@ import { EventDetails, EventType, ModuleMetadataSeederEventPayload } from 'src/i
|
|
|
86
87
|
* - View/action preload + in-memory no-op detection so unchanged rows skip save() entirely.
|
|
87
88
|
* - Menu entity no-op detection, though menu relation lookups are still mostly serial and remain a future candidate.
|
|
88
89
|
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A user entry in seed metadata.
|
|
93
|
+
*
|
|
94
|
+
* The array is genuinely mixed: core seeds `sa` (a technical user), while an app's
|
|
95
|
+
* module metadata can seed its own users - an "Operations User" and the like, which
|
|
96
|
+
* are app users and belong on the extension entity. One intent for the whole list
|
|
97
|
+
* cannot express that, so each entry declares its own.
|
|
98
|
+
*
|
|
99
|
+
* Extends SignUpDto with an index signature because a flagged entry carries the
|
|
100
|
+
* extension model's own fields (`userType`, relation keys) straight through to the
|
|
101
|
+
* provider.
|
|
102
|
+
*/
|
|
103
|
+
type SeedUserSpec = SignUpDto & {
|
|
104
|
+
/**
|
|
105
|
+
* Seed through the registered IExtensionUserCreationProvider, producing the app's
|
|
106
|
+
* extension entity instead of a base `User`. Absent means a base `User`, so
|
|
107
|
+
* existing metadata is unaffected.
|
|
108
|
+
*
|
|
109
|
+
* `roles` is ignored on a flagged entry - the provider derives them from the
|
|
110
|
+
* extension fields, as it does on the model's own CRUD form.
|
|
111
|
+
*/
|
|
112
|
+
isExtensionUser?: boolean;
|
|
113
|
+
isAllowedToGenerateApiKeys?: boolean;
|
|
114
|
+
[key: string]: any;
|
|
115
|
+
};
|
|
116
|
+
|
|
89
117
|
@Injectable()
|
|
90
118
|
export class ModuleMetadataSeederService {
|
|
91
119
|
private readonly adminPermissionExclusionPrefixes = [
|
|
@@ -758,7 +786,7 @@ export class ModuleMetadataSeederService {
|
|
|
758
786
|
|
|
759
787
|
// Ok
|
|
760
788
|
private async seedUsers(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
761
|
-
const users = this.getSeedArray<
|
|
789
|
+
const users = this.getSeedArray<SeedUserSpec>(overallMetadata?.users);
|
|
762
790
|
// usersDetail = users;
|
|
763
791
|
await this.timeOperation('handle-users', () => this.handleSeedUsers(users), {
|
|
764
792
|
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
@@ -1543,13 +1571,13 @@ export class ModuleMetadataSeederService {
|
|
|
1543
1571
|
}
|
|
1544
1572
|
|
|
1545
1573
|
// OK
|
|
1546
|
-
private async handleSeedUsers(users:
|
|
1574
|
+
private async handleSeedUsers(users: SeedUserSpec[]) {
|
|
1547
1575
|
if (!users) {
|
|
1548
1576
|
return;
|
|
1549
1577
|
}
|
|
1550
1578
|
|
|
1551
1579
|
for (let l = 0; l < users.length; l++) {
|
|
1552
|
-
const user:
|
|
1580
|
+
const user: SeedUserSpec = users[l];
|
|
1553
1581
|
const isSystemAdminUser = user.username === 'sa';
|
|
1554
1582
|
let exisitingUser = await this.timeOperation('user-find-by-username', () => this.userService.findOneByUsername(user.username), {
|
|
1555
1583
|
component: 'users',
|
|
@@ -1561,10 +1589,15 @@ export class ModuleMetadataSeederService {
|
|
|
1561
1589
|
user.password = DEFAULT_SA_PASSWORD;
|
|
1562
1590
|
}
|
|
1563
1591
|
if (isSystemAdminUser) {
|
|
1564
|
-
|
|
1592
|
+
user.isAllowedToGenerateApiKeys = true;
|
|
1565
1593
|
}
|
|
1566
1594
|
|
|
1567
|
-
|
|
1595
|
+
// The flag is seeder metadata, not user data - keep it out of the DTO
|
|
1596
|
+
// the provider and performSignUp see.
|
|
1597
|
+
const { isExtensionUser, ...signUpDto } = user;
|
|
1598
|
+
const intent = isExtensionUser ? SignupIntent.ExtensionModel : SignupIntent.CoreUser;
|
|
1599
|
+
|
|
1600
|
+
exisitingUser = await this.timeOperation('user-sign-up', () => this.authenticationService.signUp(signUpDto, null, intent), {
|
|
1568
1601
|
component: 'users',
|
|
1569
1602
|
serviceCall: 'authenticationService.signUp',
|
|
1570
1603
|
details: `username=${user.username}`,
|
|
@@ -10,6 +10,7 @@ import solidCoreMetadata from './seed-data/solid-core-metadata.json';
|
|
|
10
10
|
import { CreateModuleMetadataDto } from 'src/dtos/create-module-metadata.dto';
|
|
11
11
|
import { CreateModelMetadataDto } from 'src/dtos/create-model-metadata.dto';
|
|
12
12
|
import { MediaStorageProviderType } from 'src/dtos/create-media-storage-provider-metadata.dto';
|
|
13
|
+
import { SignupIntent } from 'src/enums/signup-intent.enum';
|
|
13
14
|
import { getDynamicModuleNamesBasedOnMetadata } from 'src/helpers/module.helper';
|
|
14
15
|
import { SolidRegistry } from 'src/helpers/solid-registry';
|
|
15
16
|
import { InternationalisationHelperService } from 'src/services/internationalisation-helper.service';
|
|
@@ -475,7 +476,14 @@ export class ModuleTestDataService {
|
|
|
475
476
|
continue;
|
|
476
477
|
}
|
|
477
478
|
|
|
478
|
-
|
|
479
|
+
// The flag is seeder metadata, not user data - keep it out of the DTO the
|
|
480
|
+
// provider and performSignUp see.
|
|
481
|
+
const { isExtensionUser, ...signUpDto } = user;
|
|
482
|
+
await authService.signUp(
|
|
483
|
+
signUpDto,
|
|
484
|
+
null,
|
|
485
|
+
isExtensionUser ? SignupIntent.ExtensionModel : SignupIntent.CoreUser,
|
|
486
|
+
);
|
|
479
487
|
this.logger.log(`Created test user "${user.username}"${user.roles?.length ? ` with roles [${user.roles.join(', ')}]` : ''}`);
|
|
480
488
|
}
|
|
481
489
|
}
|
|
@@ -36,6 +36,7 @@ import { OTPSignUpDto } from "../dtos/otp-sign-up.dto";
|
|
|
36
36
|
import { RefreshTokenDto } from "../dtos/refresh-token.dto";
|
|
37
37
|
import { SignInDto } from "../dtos/sign-in.dto";
|
|
38
38
|
import { SignUpDto } from "../dtos/sign-up.dto";
|
|
39
|
+
import { SignupIntent } from "../enums/signup-intent.enum";
|
|
39
40
|
import { User } from "../entities/user.entity";
|
|
40
41
|
import { EventDetails, EventType } from "../interfaces";
|
|
41
42
|
import { ActiveUserData } from "../interfaces/active-user-data.interface";
|
|
@@ -67,6 +68,60 @@ interface otp {
|
|
|
67
68
|
expiresAt: Date;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/** Where a signup's role names come from. */
|
|
72
|
+
enum RolesSource {
|
|
73
|
+
/**
|
|
74
|
+
* Ask nobody; returning [] lets performSignUp apply the configured `defaultRole`.
|
|
75
|
+
*
|
|
76
|
+
* No intent maps here today - it is kept for paths that deliberately bypass the
|
|
77
|
+
* provider. OAuth is the live example: its DTO can never carry a discriminator, so
|
|
78
|
+
* it takes `defaultRole` outright (currently inline in UserService).
|
|
79
|
+
*/
|
|
80
|
+
Default,
|
|
81
|
+
/**
|
|
82
|
+
* `provider.roles(dto)` - and nothing else. Yields [] when no provider is registered
|
|
83
|
+
* or when the provider declines to name any, which falls through to `defaultRole`.
|
|
84
|
+
*/
|
|
85
|
+
Provider,
|
|
86
|
+
/** The caller's own `dto.roles`. */
|
|
87
|
+
Caller,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The single place that decides, per intent, which entity a signup builds and where
|
|
92
|
+
* its roles come from.
|
|
93
|
+
*
|
|
94
|
+
* Where an extension user provider is registered it is the authority on that app's user
|
|
95
|
+
* roles, so every provider-backed intent asks it. `defaultRole` is the *fallback* for
|
|
96
|
+
* when it names none - or when there is no provider at all - not the rule.
|
|
97
|
+
*
|
|
98
|
+
* SelfRegistration and ExtensionModel resolve identically today. They stay separate
|
|
99
|
+
* intents because only SelfRegistration is gated on `allowPublicRegistration`, and
|
|
100
|
+
* because "an anonymous visitor signing themselves up" and "an admin creating a user
|
|
101
|
+
* from the model's own form" are different things that may yet need to diverge.
|
|
102
|
+
*
|
|
103
|
+
* `useProvider` is computable from `rolesSource` today (`Caller` implies false), but the
|
|
104
|
+
* two answer different questions - "which entity?" and "which roles?" - that merely
|
|
105
|
+
* coincide across these intents. Keep both, and change behaviour here, not at a call site.
|
|
106
|
+
*/
|
|
107
|
+
const SIGNUP_POLICY: Record<
|
|
108
|
+
SignupIntent,
|
|
109
|
+
{ useProvider: boolean; rolesSource: RolesSource }
|
|
110
|
+
> = {
|
|
111
|
+
[SignupIntent.SelfRegistration]: {
|
|
112
|
+
useProvider: true,
|
|
113
|
+
rolesSource: RolesSource.Provider,
|
|
114
|
+
},
|
|
115
|
+
[SignupIntent.ExtensionModel]: {
|
|
116
|
+
useProvider: true,
|
|
117
|
+
rolesSource: RolesSource.Provider,
|
|
118
|
+
},
|
|
119
|
+
[SignupIntent.CoreUser]: {
|
|
120
|
+
useProvider: false,
|
|
121
|
+
rolesSource: RolesSource.Caller,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
70
125
|
@Injectable()
|
|
71
126
|
export class AuthenticationService {
|
|
72
127
|
private readonly logger = new Logger(AuthenticationService.name);
|
|
@@ -184,52 +239,69 @@ export class AuthenticationService {
|
|
|
184
239
|
}
|
|
185
240
|
}
|
|
186
241
|
|
|
187
|
-
private static readonly SIGNUP_DTO_KEYS = new Set([
|
|
188
|
-
"username",
|
|
189
|
-
"email",
|
|
190
|
-
"password",
|
|
191
|
-
"fullName",
|
|
192
|
-
"mobile",
|
|
193
|
-
"roles",
|
|
194
|
-
"forcePasswordChange",
|
|
195
|
-
"isAllowedToGenerateApiKeys",
|
|
196
|
-
"failedLoginAttempts",
|
|
197
|
-
]);
|
|
198
|
-
|
|
199
242
|
async signUp(
|
|
200
243
|
signUpDto: SignUpDto & Record<string, any>,
|
|
201
244
|
activeUser: ActiveUserData = null,
|
|
245
|
+
intent: SignupIntent = SignupIntent.SelfRegistration,
|
|
202
246
|
): Promise<User> {
|
|
203
|
-
const
|
|
204
|
-
|
|
247
|
+
const { useProvider, rolesSource } = SIGNUP_POLICY[intent];
|
|
248
|
+
|
|
249
|
+
if (intent === SignupIntent.SelfRegistration) {
|
|
250
|
+
this.assertPublicRegistrationEnabled();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const { entity, repo } = await this.userService.buildSignupTarget(
|
|
254
|
+
signUpDto,
|
|
255
|
+
useProvider,
|
|
205
256
|
);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
const roles = this.resolveSignupRoles(signUpDto, rolesSource);
|
|
258
|
+
|
|
259
|
+
return this.performSignUp({ ...signUpDto, roles }, entity, repo);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private resolveSignupRoles(
|
|
263
|
+
dto: Record<string, any>,
|
|
264
|
+
source: RolesSource,
|
|
265
|
+
): string[] {
|
|
266
|
+
// Only `Caller` reads dto.roles. Anywhere else, roles in the body are ignored -
|
|
267
|
+
// which is what keeps an anonymous caller from naming their own on a @Public()
|
|
268
|
+
// endpoint - so say so rather than dropping them silently.
|
|
269
|
+
if (source !== RolesSource.Caller && dto.roles?.length) {
|
|
270
|
+
this.logger.warn(
|
|
271
|
+
`Ignoring caller-supplied roles for "${dto.username}": roles on this path come from ` +
|
|
272
|
+
(source === RolesSource.Provider
|
|
273
|
+
? "the extension user provider"
|
|
274
|
+
: "the configured defaultRole"),
|
|
220
275
|
);
|
|
221
276
|
}
|
|
222
|
-
|
|
277
|
+
|
|
278
|
+
switch (source) {
|
|
279
|
+
case RolesSource.Provider:
|
|
280
|
+
// Sole authority. Reading dto.roles as a preference or fallback would skip
|
|
281
|
+
// roles(), which is where a provider validates its discriminator, and
|
|
282
|
+
// CreateUserDto types roles as UpdateRoleMetadataDto[] where performSignUp
|
|
283
|
+
// expects role-name strings. [] here falls through to `defaultRole`.
|
|
284
|
+
return (
|
|
285
|
+
this.solidRegistry
|
|
286
|
+
.getExtensionUserCreationProvider()
|
|
287
|
+
?.roles(dto as any) ?? []
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
case RolesSource.Caller:
|
|
291
|
+
return dto.roles ?? [];
|
|
292
|
+
|
|
293
|
+
case RolesSource.Default:
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
223
296
|
}
|
|
224
297
|
|
|
225
298
|
private async performSignUp<T extends User>(
|
|
226
299
|
signUpDto: SignUpDto,
|
|
227
300
|
entity: T,
|
|
228
301
|
repo: Repository<T>,
|
|
229
|
-
preferEntityApiKeyFlag: boolean = false,
|
|
230
302
|
): Promise<T> {
|
|
231
303
|
try {
|
|
232
|
-
await this.assertUniqueSignupIdentifiers(signUpDto
|
|
304
|
+
await this.assertUniqueSignupIdentifiers(signUpDto);
|
|
233
305
|
await this.metadataValidationService.validateCreateDto("user", signUpDto);
|
|
234
306
|
|
|
235
307
|
const onForcePasswordChange =
|
|
@@ -249,11 +321,11 @@ export class AuthenticationService {
|
|
|
249
321
|
activateUserOnRegistration,
|
|
250
322
|
onForcePasswordChange,
|
|
251
323
|
);
|
|
324
|
+
// An explicitly supplied value wins over whatever the entity carries. The
|
|
325
|
+
// entity's own flag cannot be trusted as a signal: User initialises it to
|
|
326
|
+
// false, so "the provider set it" is indistinguishable from "nobody set it".
|
|
252
327
|
const privateDto = signUpDto as { isAllowedToGenerateApiKeys?: boolean };
|
|
253
|
-
if (
|
|
254
|
-
!preferEntityApiKeyFlag &&
|
|
255
|
-
privateDto.isAllowedToGenerateApiKeys !== undefined
|
|
256
|
-
) {
|
|
328
|
+
if (privateDto.isAllowedToGenerateApiKeys !== undefined) {
|
|
257
329
|
user.isAllowedToGenerateApiKeys = privateDto.isAllowedToGenerateApiKeys;
|
|
258
330
|
}
|
|
259
331
|
const savedUser = await repo.save(user);
|
|
@@ -283,33 +355,41 @@ export class AuthenticationService {
|
|
|
283
355
|
}
|
|
284
356
|
}
|
|
285
357
|
|
|
286
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Always queries the base `User` repository, never the caller's repository.
|
|
360
|
+
*
|
|
361
|
+
* `User` is a `@TableInheritance` root, so a child repository scopes every query
|
|
362
|
+
* to its own discriminator. Checking through one would only compare against users
|
|
363
|
+
* of the same subtype, and `email`/`mobile` carry non-unique `@Index()` - there is
|
|
364
|
+
* no database constraint behind them to catch what the query misses. A duplicate
|
|
365
|
+
* against a base `User` or a sibling subtype would be silently accepted.
|
|
366
|
+
*/
|
|
367
|
+
private async assertUniqueSignupIdentifiers(
|
|
287
368
|
signUpDto: SignUpDto,
|
|
288
|
-
repo: Repository<T>,
|
|
289
369
|
): Promise<void> {
|
|
290
370
|
const username = signUpDto.username?.trim();
|
|
291
371
|
const email = signUpDto.email?.trim();
|
|
292
372
|
const mobile = signUpDto.mobile?.trim();
|
|
293
373
|
|
|
294
|
-
const where: FindOptionsWhere<
|
|
374
|
+
const where: FindOptionsWhere<User>[] = [];
|
|
295
375
|
|
|
296
376
|
if (username) {
|
|
297
|
-
where.push({ username }
|
|
377
|
+
where.push({ username });
|
|
298
378
|
}
|
|
299
379
|
|
|
300
380
|
if (email) {
|
|
301
|
-
where.push({ email }
|
|
381
|
+
where.push({ email });
|
|
302
382
|
}
|
|
303
383
|
|
|
304
384
|
if (mobile) {
|
|
305
|
-
where.push({ mobile }
|
|
385
|
+
where.push({ mobile });
|
|
306
386
|
}
|
|
307
387
|
|
|
308
388
|
if (where.length === 0) {
|
|
309
389
|
return;
|
|
310
390
|
}
|
|
311
391
|
|
|
312
|
-
const existingUser = await
|
|
392
|
+
const existingUser = await this.userRepository.findOne({ where });
|
|
313
393
|
|
|
314
394
|
if (!existingUser) {
|
|
315
395
|
return;
|
|
@@ -401,7 +481,12 @@ export class AuthenticationService {
|
|
|
401
481
|
}
|
|
402
482
|
user.username = signUpDto.username;
|
|
403
483
|
user.email = signUpDto.email;
|
|
404
|
-
|
|
484
|
+
// `fullName` is optional on SignUpDto and the stock signup screen only sends it when
|
|
485
|
+
// showNameFieldsForRegistration is on, so assigning it unconditionally left every
|
|
486
|
+
// self-registered user with a null display name. `username` is always present -
|
|
487
|
+
// non-nullable on the entity and @IsNotEmpty() on the DTO - so it is a safe fallback.
|
|
488
|
+
// `||` rather than `??`: a form posting an empty string means "not supplied" too.
|
|
489
|
+
user.fullName = signUpDto.fullName?.trim() || signUpDto.username;
|
|
405
490
|
user.forcePasswordChange = onForcePasswordChange;
|
|
406
491
|
if (signUpDto.mobile) {
|
|
407
492
|
user.mobile = signUpDto.mobile;
|
|
@@ -602,7 +687,32 @@ export class AuthenticationService {
|
|
|
602
687
|
}
|
|
603
688
|
}
|
|
604
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Gates *self-service* account creation only - the public register endpoints, where
|
|
692
|
+
* an anonymous visitor creates their own account. Callers that create a user on
|
|
693
|
+
* someone else's behalf (the admin console, an extension model's CRUD form, the
|
|
694
|
+
* seeders) are deliberately unaffected, so turning this off does not disable user
|
|
695
|
+
* creation across the system.
|
|
696
|
+
*
|
|
697
|
+
* Compared against both representations because `getConfigValue` returns the raw
|
|
698
|
+
* cached value, which is a boolean when it comes from the provider default and a
|
|
699
|
+
* string once persisted or edited through the Settings screen - see the same
|
|
700
|
+
* defensive comparison for `mcpEnabled` in setting.service.ts. A plain falsy check
|
|
701
|
+
* would read `'false'` as truthy and never fire.
|
|
702
|
+
*/
|
|
703
|
+
private assertPublicRegistrationEnabled(): void {
|
|
704
|
+
const allowPublicRegistration =
|
|
705
|
+
this.settingService.getConfigValue<SolidCoreSetting>(
|
|
706
|
+
"allowPublicRegistration",
|
|
707
|
+
);
|
|
708
|
+
if (allowPublicRegistration === false || allowPublicRegistration === "false") {
|
|
709
|
+
throw new ForbiddenException(ERROR_MESSAGES.PUBLIC_REGISTRATION_DISABLED);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
605
713
|
async otpInitiateRegistration(signUpDto: OTPSignUpDto) {
|
|
714
|
+
this.assertPublicRegistrationEnabled();
|
|
715
|
+
|
|
606
716
|
const isPasswordlessRegistrationEnabled =
|
|
607
717
|
await this.isPasswordlessRegistrationEnabled();
|
|
608
718
|
if (!isPasswordlessRegistrationEnabled) {
|
|
@@ -682,32 +792,68 @@ export class AuthenticationService {
|
|
|
682
792
|
signUpDto: OTPSignUpDto,
|
|
683
793
|
validationSource: string,
|
|
684
794
|
): Promise<User> {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
user.username,
|
|
693
|
-
this.settingService.getConfigValue<SolidCoreSetting>("defaultRole"),
|
|
795
|
+
if (isEmpty(existingUser)) {
|
|
796
|
+
// Resolved before anything is written: where a provider requires its discriminator
|
|
797
|
+
// roles() throws, and that should surface as a rejected request rather than as a
|
|
798
|
+
// half-registered user with no roles.
|
|
799
|
+
const roles = this.resolveSignupRoles(
|
|
800
|
+
signUpDto,
|
|
801
|
+
SIGNUP_POLICY[SignupIntent.SelfRegistration].rolesSource,
|
|
694
802
|
);
|
|
695
|
-
|
|
803
|
+
|
|
804
|
+
// A new registration is saved through whichever repository createUser resolved,
|
|
805
|
+
// which is the provider's when the app registers one.
|
|
806
|
+
const { entity: user, repo } = await this.createUser(signUpDto);
|
|
807
|
+
user.active = false; // User will be activated only after OTP verification, hence setting active to false for new user.
|
|
696
808
|
await this.assignRegistrationOtp(validationSource, user);
|
|
697
|
-
await
|
|
809
|
+
await repo.save(user);
|
|
810
|
+
|
|
811
|
+
if (roles.length) {
|
|
812
|
+
await this.userService.addRolesToUser(user.username, roles);
|
|
813
|
+
} else {
|
|
814
|
+
// The provider named none, or there is no provider: fall back to the configured
|
|
815
|
+
// default, matching what performSignUp does on the password paths.
|
|
816
|
+
await this.userService.addRoleToUser(
|
|
817
|
+
user.username,
|
|
818
|
+
this.settingService.getConfigValue<SolidCoreSetting>("defaultRole"),
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
return user;
|
|
698
822
|
}
|
|
823
|
+
|
|
824
|
+
// An existing row is saved back through the base repository: `User` is the
|
|
825
|
+
// inheritance root, so TypeORM hydrated it as its own subclass on the way in and
|
|
826
|
+
// round-trips the discriminator on the way out.
|
|
827
|
+
const user = existingUser;
|
|
828
|
+
await this.assignRegistrationOtp(validationSource, user);
|
|
829
|
+
await this.userRepository.save(user);
|
|
699
830
|
return user;
|
|
700
831
|
}
|
|
701
832
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
833
|
+
/**
|
|
834
|
+
* Creates a new user entity for OTP registration - of whichever type this app
|
|
835
|
+
* registers its users as, since passwordless signup is self-registration like any
|
|
836
|
+
* other. Returns the repository alongside it, because a provider-built entity has
|
|
837
|
+
* to be saved through the provider's own repository.
|
|
838
|
+
*/
|
|
839
|
+
private async createUser(
|
|
840
|
+
signUpDto: OTPSignUpDto,
|
|
841
|
+
): Promise<{ entity: User; repo: Repository<User> }> {
|
|
842
|
+
const { useProvider } = SIGNUP_POLICY[SignupIntent.SelfRegistration];
|
|
843
|
+
const { entity, repo } = await this.userService.buildSignupTarget(
|
|
844
|
+
signUpDto,
|
|
845
|
+
useProvider,
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
entity.username = signUpDto.username;
|
|
849
|
+
entity.email = signUpDto.email;
|
|
850
|
+
entity.mobile = signUpDto.mobile;
|
|
851
|
+
// OTPSignUpDto declares no `fullName`, so username is the only source here. Mirrors the fallback in populateForSignup, which this path does not go through.
|
|
852
|
+
entity.fullName = signUpDto.fullName?.trim() || signUpDto.username;
|
|
853
|
+
entity.customPayload = signUpDto.customPayload;
|
|
854
|
+
entity.lastLoginProvider = LoginProvider.OTP;
|
|
855
|
+
|
|
856
|
+
return { entity, repo };
|
|
711
857
|
}
|
|
712
858
|
|
|
713
859
|
// Generate the validation tokens for the user i.e (system configured + user provided)
|
|
@@ -3,6 +3,7 @@ import { DiscoveryService, ModuleRef } from "@nestjs/core";
|
|
|
3
3
|
import { isArray } from "class-validator";
|
|
4
4
|
import { CommonEntity } from "../entities/common.entity";
|
|
5
5
|
import { User } from "../entities/user.entity";
|
|
6
|
+
import { SignupIntent } from "../enums/signup-intent.enum";
|
|
6
7
|
import { SolidBaseRepository } from "../repository/solid-base.repository";
|
|
7
8
|
import { SettingService } from "./setting.service";
|
|
8
9
|
import { ERROR_MESSAGES } from "src/constants/error-messages";
|
|
@@ -113,7 +114,7 @@ export class CRUDService<T extends CommonEntity> { // Add two generic value i.e
|
|
|
113
114
|
}
|
|
114
115
|
const { AuthenticationService } = await import('./authentication.service');
|
|
115
116
|
const authService = this.moduleRef.get(AuthenticationService, { strict: false });
|
|
116
|
-
return authService.signUp(createDto) as unknown as T;
|
|
117
|
+
return authService.signUp(createDto, null, SignupIntent.ExtensionModel) as unknown as T;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
async create(createDto: any, files: Express.Multer.File[] = [], solidRequestContext: any = {}): Promise<T> {
|
|
@@ -15,6 +15,7 @@ import { RoleMetadata } from "../entities/role-metadata.entity";
|
|
|
15
15
|
import { User } from "../entities/user.entity";
|
|
16
16
|
import { ActiveUserData } from "../interfaces/active-user-data.interface";
|
|
17
17
|
import { ERROR_MESSAGES } from "src/constants/error-messages";
|
|
18
|
+
import { SolidRegistry } from "src/helpers/solid-registry";
|
|
18
19
|
import { UserRepository } from "src/repository/user.repository";
|
|
19
20
|
import { RoleMetadataRepository } from "src/repository/role-metadata.repository";
|
|
20
21
|
import { HashingService } from "./hashing.service";
|
|
@@ -111,6 +112,39 @@ export class UserService extends CRUDService<User> {
|
|
|
111
112
|
return super.deleteMany(ids, solidRequestContext);
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Resolves which entity a signup should build and which repository it should be
|
|
117
|
+
* saved through.
|
|
118
|
+
*
|
|
119
|
+
* When `useProvider` is set and the app registered an `IExtensionUserCreationProvider`,
|
|
120
|
+
* the provider builds the entity and owns the repository; otherwise a plain `User`
|
|
121
|
+
* is saved through the base repository. The caller decides `useProvider` from its
|
|
122
|
+
* `SignupIntent` - nothing here inspects the DTO.
|
|
123
|
+
*
|
|
124
|
+
* The registry is resolved lazily through `moduleRef` rather than injected, mirroring
|
|
125
|
+
* `CRUDService.tryCreateAsExtensionUser`, to keep this off the DI graph that
|
|
126
|
+
* AuthenticationService and CRUDService already form a cycle around.
|
|
127
|
+
*/
|
|
128
|
+
async buildSignupTarget(
|
|
129
|
+
dto: Record<string, any>,
|
|
130
|
+
useProvider: boolean,
|
|
131
|
+
): Promise<{ entity: User; repo: Repository<User> }> {
|
|
132
|
+
const provider = useProvider
|
|
133
|
+
? this.moduleRef
|
|
134
|
+
.get(SolidRegistry, { strict: false })
|
|
135
|
+
?.getExtensionUserCreationProvider()
|
|
136
|
+
: null;
|
|
137
|
+
|
|
138
|
+
if (!provider) {
|
|
139
|
+
return { entity: new User(), repo: this.repo };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
entity: await provider.buildExtensionEntity(dto as any),
|
|
144
|
+
repo: provider.repo as Repository<User>,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
114
148
|
async findOneByEmail(email: string): Promise<User> {
|
|
115
149
|
return await this.repo.findOne({
|
|
116
150
|
where: {
|
|
@@ -18,6 +18,18 @@ export interface TestingUserSpec {
|
|
|
18
18
|
fullName?: string;
|
|
19
19
|
mobile?: string;
|
|
20
20
|
roles?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Seed this user through the registered IExtensionUserCreationProvider, making it
|
|
23
|
+
* an instance of the app's extension entity rather than a base `User`.
|
|
24
|
+
*
|
|
25
|
+
* Absent means a base `User`, so existing specs are unaffected. Set it on users
|
|
26
|
+
* that carry extension fields (`userType` and the like) - without it those fields
|
|
27
|
+
* are ignored and a plain `User` is created.
|
|
28
|
+
*
|
|
29
|
+
* `roles` is ignored on a flagged user: the provider derives them from the
|
|
30
|
+
* extension fields, exactly as it does on the model's own CRUD form.
|
|
31
|
+
*/
|
|
32
|
+
isExtensionUser?: boolean;
|
|
21
33
|
[key: string]: any;
|
|
22
34
|
}
|
|
23
35
|
|