@solidxai/core 0.1.16-beta.1 → 0.1.16-beta.2
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 +657 -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/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 +73 -45
- 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 +2 -0
- package/src/controllers/authentication.controller.ts +5 -2
- 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 +170 -59
- 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
|
@@ -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,43 @@ interface otp {
|
|
|
67
68
|
expiresAt: Date;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/** Where a signup's role names come from. */
|
|
72
|
+
enum RolesSource {
|
|
73
|
+
/** None from the caller: performSignUp applies the configured `defaultRole`. */
|
|
74
|
+
Default,
|
|
75
|
+
/** `provider.roles(dto)`, unconditionally. */
|
|
76
|
+
Provider,
|
|
77
|
+
/** The caller's own `dto.roles`. */
|
|
78
|
+
Caller,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The single place that decides, per intent, which entity a signup builds and where
|
|
83
|
+
* its roles come from.
|
|
84
|
+
*
|
|
85
|
+
* `useProvider` is currently computable from `rolesSource` (`Caller` implies false),
|
|
86
|
+
* but the two answer different questions - "which entity?" and "which roles?" - that
|
|
87
|
+
* merely coincide across these three intents. Keep both, and change behaviour here
|
|
88
|
+
* rather than at a call site.
|
|
89
|
+
*/
|
|
90
|
+
const SIGNUP_POLICY: Record<
|
|
91
|
+
SignupIntent,
|
|
92
|
+
{ useProvider: boolean; rolesSource: RolesSource }
|
|
93
|
+
> = {
|
|
94
|
+
[SignupIntent.SelfRegistration]: {
|
|
95
|
+
useProvider: true,
|
|
96
|
+
rolesSource: RolesSource.Default,
|
|
97
|
+
},
|
|
98
|
+
[SignupIntent.ExtensionModel]: {
|
|
99
|
+
useProvider: true,
|
|
100
|
+
rolesSource: RolesSource.Provider,
|
|
101
|
+
},
|
|
102
|
+
[SignupIntent.CoreUser]: {
|
|
103
|
+
useProvider: false,
|
|
104
|
+
rolesSource: RolesSource.Caller,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
70
108
|
@Injectable()
|
|
71
109
|
export class AuthenticationService {
|
|
72
110
|
private readonly logger = new Logger(AuthenticationService.name);
|
|
@@ -184,52 +222,65 @@ export class AuthenticationService {
|
|
|
184
222
|
}
|
|
185
223
|
}
|
|
186
224
|
|
|
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
225
|
async signUp(
|
|
200
226
|
signUpDto: SignUpDto & Record<string, any>,
|
|
201
227
|
activeUser: ActiveUserData = null,
|
|
228
|
+
intent: SignupIntent = SignupIntent.SelfRegistration,
|
|
202
229
|
): Promise<User> {
|
|
203
|
-
const
|
|
204
|
-
|
|
230
|
+
const { useProvider, rolesSource } = SIGNUP_POLICY[intent];
|
|
231
|
+
|
|
232
|
+
if (intent === SignupIntent.SelfRegistration) {
|
|
233
|
+
this.assertPublicRegistrationEnabled();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { entity, repo } = await this.userService.buildSignupTarget(
|
|
237
|
+
signUpDto,
|
|
238
|
+
useProvider,
|
|
205
239
|
);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
240
|
+
const roles = this.resolveSignupRoles(signUpDto, rolesSource);
|
|
241
|
+
|
|
242
|
+
return this.performSignUp({ ...signUpDto, roles }, entity, repo);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private resolveSignupRoles(
|
|
246
|
+
dto: Record<string, any>,
|
|
247
|
+
source: RolesSource,
|
|
248
|
+
): string[] {
|
|
249
|
+
switch (source) {
|
|
250
|
+
case RolesSource.Provider:
|
|
251
|
+
// Sole authority. `dto.roles` is deliberately not read - not as a preference,
|
|
252
|
+
// not as a fallback. Reading it would skip roles(), which is where a provider
|
|
253
|
+
// validates its discriminator, and CreateUserDto types roles as
|
|
254
|
+
// UpdateRoleMetadataDto[] where performSignUp expects role-name strings.
|
|
255
|
+
return (
|
|
256
|
+
this.solidRegistry
|
|
257
|
+
.getExtensionUserCreationProvider()
|
|
258
|
+
?.roles(dto as any) ?? []
|
|
211
259
|
);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
260
|
+
|
|
261
|
+
case RolesSource.Caller:
|
|
262
|
+
return dto.roles ?? [];
|
|
263
|
+
|
|
264
|
+
case RolesSource.Default:
|
|
265
|
+
// Public signup: the form has no business naming roles. Returning empty lets
|
|
266
|
+
// performSignUp apply the configured `defaultRole`, rather than adding a
|
|
267
|
+
// second mechanism for the same thing.
|
|
268
|
+
if (dto.roles?.length) {
|
|
269
|
+
this.logger.warn(
|
|
270
|
+
`Ignoring caller-supplied roles on public registration for "${dto.username}"`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return [];
|
|
221
274
|
}
|
|
222
|
-
return this.performSignUp(signUpDto, new User(), this.userRepository);
|
|
223
275
|
}
|
|
224
276
|
|
|
225
277
|
private async performSignUp<T extends User>(
|
|
226
278
|
signUpDto: SignUpDto,
|
|
227
279
|
entity: T,
|
|
228
280
|
repo: Repository<T>,
|
|
229
|
-
preferEntityApiKeyFlag: boolean = false,
|
|
230
281
|
): Promise<T> {
|
|
231
282
|
try {
|
|
232
|
-
await this.assertUniqueSignupIdentifiers(signUpDto
|
|
283
|
+
await this.assertUniqueSignupIdentifiers(signUpDto);
|
|
233
284
|
await this.metadataValidationService.validateCreateDto("user", signUpDto);
|
|
234
285
|
|
|
235
286
|
const onForcePasswordChange =
|
|
@@ -249,11 +300,11 @@ export class AuthenticationService {
|
|
|
249
300
|
activateUserOnRegistration,
|
|
250
301
|
onForcePasswordChange,
|
|
251
302
|
);
|
|
303
|
+
// An explicitly supplied value wins over whatever the entity carries. The
|
|
304
|
+
// entity's own flag cannot be trusted as a signal: User initialises it to
|
|
305
|
+
// false, so "the provider set it" is indistinguishable from "nobody set it".
|
|
252
306
|
const privateDto = signUpDto as { isAllowedToGenerateApiKeys?: boolean };
|
|
253
|
-
if (
|
|
254
|
-
!preferEntityApiKeyFlag &&
|
|
255
|
-
privateDto.isAllowedToGenerateApiKeys !== undefined
|
|
256
|
-
) {
|
|
307
|
+
if (privateDto.isAllowedToGenerateApiKeys !== undefined) {
|
|
257
308
|
user.isAllowedToGenerateApiKeys = privateDto.isAllowedToGenerateApiKeys;
|
|
258
309
|
}
|
|
259
310
|
const savedUser = await repo.save(user);
|
|
@@ -283,33 +334,41 @@ export class AuthenticationService {
|
|
|
283
334
|
}
|
|
284
335
|
}
|
|
285
336
|
|
|
286
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Always queries the base `User` repository, never the caller's repository.
|
|
339
|
+
*
|
|
340
|
+
* `User` is a `@TableInheritance` root, so a child repository scopes every query
|
|
341
|
+
* to its own discriminator. Checking through one would only compare against users
|
|
342
|
+
* of the same subtype, and `email`/`mobile` carry non-unique `@Index()` - there is
|
|
343
|
+
* no database constraint behind them to catch what the query misses. A duplicate
|
|
344
|
+
* against a base `User` or a sibling subtype would be silently accepted.
|
|
345
|
+
*/
|
|
346
|
+
private async assertUniqueSignupIdentifiers(
|
|
287
347
|
signUpDto: SignUpDto,
|
|
288
|
-
repo: Repository<T>,
|
|
289
348
|
): Promise<void> {
|
|
290
349
|
const username = signUpDto.username?.trim();
|
|
291
350
|
const email = signUpDto.email?.trim();
|
|
292
351
|
const mobile = signUpDto.mobile?.trim();
|
|
293
352
|
|
|
294
|
-
const where: FindOptionsWhere<
|
|
353
|
+
const where: FindOptionsWhere<User>[] = [];
|
|
295
354
|
|
|
296
355
|
if (username) {
|
|
297
|
-
where.push({ username }
|
|
356
|
+
where.push({ username });
|
|
298
357
|
}
|
|
299
358
|
|
|
300
359
|
if (email) {
|
|
301
|
-
where.push({ email }
|
|
360
|
+
where.push({ email });
|
|
302
361
|
}
|
|
303
362
|
|
|
304
363
|
if (mobile) {
|
|
305
|
-
where.push({ mobile }
|
|
364
|
+
where.push({ mobile });
|
|
306
365
|
}
|
|
307
366
|
|
|
308
367
|
if (where.length === 0) {
|
|
309
368
|
return;
|
|
310
369
|
}
|
|
311
370
|
|
|
312
|
-
const existingUser = await
|
|
371
|
+
const existingUser = await this.userRepository.findOne({ where });
|
|
313
372
|
|
|
314
373
|
if (!existingUser) {
|
|
315
374
|
return;
|
|
@@ -401,7 +460,12 @@ export class AuthenticationService {
|
|
|
401
460
|
}
|
|
402
461
|
user.username = signUpDto.username;
|
|
403
462
|
user.email = signUpDto.email;
|
|
404
|
-
|
|
463
|
+
// `fullName` is optional on SignUpDto and the stock signup screen only sends it when
|
|
464
|
+
// showNameFieldsForRegistration is on, so assigning it unconditionally left every
|
|
465
|
+
// self-registered user with a null display name. `username` is always present -
|
|
466
|
+
// non-nullable on the entity and @IsNotEmpty() on the DTO - so it is a safe fallback.
|
|
467
|
+
// `||` rather than `??`: a form posting an empty string means "not supplied" too.
|
|
468
|
+
user.fullName = signUpDto.fullName?.trim() || signUpDto.username;
|
|
405
469
|
user.forcePasswordChange = onForcePasswordChange;
|
|
406
470
|
if (signUpDto.mobile) {
|
|
407
471
|
user.mobile = signUpDto.mobile;
|
|
@@ -602,7 +666,32 @@ export class AuthenticationService {
|
|
|
602
666
|
}
|
|
603
667
|
}
|
|
604
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Gates *self-service* account creation only - the public register endpoints, where
|
|
671
|
+
* an anonymous visitor creates their own account. Callers that create a user on
|
|
672
|
+
* someone else's behalf (the admin console, an extension model's CRUD form, the
|
|
673
|
+
* seeders) are deliberately unaffected, so turning this off does not disable user
|
|
674
|
+
* creation across the system.
|
|
675
|
+
*
|
|
676
|
+
* Compared against both representations because `getConfigValue` returns the raw
|
|
677
|
+
* cached value, which is a boolean when it comes from the provider default and a
|
|
678
|
+
* string once persisted or edited through the Settings screen - see the same
|
|
679
|
+
* defensive comparison for `mcpEnabled` in setting.service.ts. A plain falsy check
|
|
680
|
+
* would read `'false'` as truthy and never fire.
|
|
681
|
+
*/
|
|
682
|
+
private assertPublicRegistrationEnabled(): void {
|
|
683
|
+
const allowPublicRegistration =
|
|
684
|
+
this.settingService.getConfigValue<SolidCoreSetting>(
|
|
685
|
+
"allowPublicRegistration",
|
|
686
|
+
);
|
|
687
|
+
if (allowPublicRegistration === false || allowPublicRegistration === "false") {
|
|
688
|
+
throw new ForbiddenException(ERROR_MESSAGES.PUBLIC_REGISTRATION_DISABLED);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
605
692
|
async otpInitiateRegistration(signUpDto: OTPSignUpDto) {
|
|
693
|
+
this.assertPublicRegistrationEnabled();
|
|
694
|
+
|
|
606
695
|
const isPasswordlessRegistrationEnabled =
|
|
607
696
|
await this.isPasswordlessRegistrationEnabled();
|
|
608
697
|
if (!isPasswordlessRegistrationEnabled) {
|
|
@@ -682,32 +771,54 @@ export class AuthenticationService {
|
|
|
682
771
|
signUpDto: OTPSignUpDto,
|
|
683
772
|
validationSource: string,
|
|
684
773
|
): Promise<User> {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
774
|
+
if (isEmpty(existingUser)) {
|
|
775
|
+
// A new registration is saved through whichever repository createUser resolved,
|
|
776
|
+
// which is the provider's when the app registers one.
|
|
777
|
+
const { entity: user, repo } = await this.createUser(signUpDto);
|
|
688
778
|
user.active = false; // User will be activated only after OTP verification, hence setting active to false for new user.
|
|
689
779
|
await this.assignRegistrationOtp(validationSource, user);
|
|
690
|
-
await
|
|
780
|
+
await repo.save(user);
|
|
691
781
|
await this.userService.addRoleToUser(
|
|
692
782
|
user.username,
|
|
693
783
|
this.settingService.getConfigValue<SolidCoreSetting>("defaultRole"),
|
|
694
784
|
);
|
|
695
|
-
|
|
696
|
-
await this.assignRegistrationOtp(validationSource, user);
|
|
697
|
-
await this.userRepository.save(user);
|
|
785
|
+
return user;
|
|
698
786
|
}
|
|
787
|
+
|
|
788
|
+
// An existing row is saved back through the base repository: `User` is the
|
|
789
|
+
// inheritance root, so TypeORM hydrated it as its own subclass on the way in and
|
|
790
|
+
// round-trips the discriminator on the way out.
|
|
791
|
+
const user = existingUser;
|
|
792
|
+
await this.assignRegistrationOtp(validationSource, user);
|
|
793
|
+
await this.userRepository.save(user);
|
|
699
794
|
return user;
|
|
700
795
|
}
|
|
701
796
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
797
|
+
/**
|
|
798
|
+
* Creates a new user entity for OTP registration - of whichever type this app
|
|
799
|
+
* registers its users as, since passwordless signup is self-registration like any
|
|
800
|
+
* other. Returns the repository alongside it, because a provider-built entity has
|
|
801
|
+
* to be saved through the provider's own repository.
|
|
802
|
+
*/
|
|
803
|
+
private async createUser(
|
|
804
|
+
signUpDto: OTPSignUpDto,
|
|
805
|
+
): Promise<{ entity: User; repo: Repository<User> }> {
|
|
806
|
+
const { useProvider } = SIGNUP_POLICY[SignupIntent.SelfRegistration];
|
|
807
|
+
const { entity, repo } = await this.userService.buildSignupTarget(
|
|
808
|
+
signUpDto,
|
|
809
|
+
useProvider,
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
entity.username = signUpDto.username;
|
|
813
|
+
entity.email = signUpDto.email;
|
|
814
|
+
entity.mobile = signUpDto.mobile;
|
|
815
|
+
// OTPSignUpDto declares no `fullName`, so username is the only source here. Mirrors
|
|
816
|
+
// the fallback in populateForSignup, which this path does not go through.
|
|
817
|
+
entity.fullName = signUpDto.username;
|
|
818
|
+
entity.customPayload = signUpDto.customPayload;
|
|
819
|
+
entity.lastLoginProvider = LoginProvider.OTP;
|
|
820
|
+
|
|
821
|
+
return { entity, repo };
|
|
711
822
|
}
|
|
712
823
|
|
|
713
824
|
// 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
|
|