@solidxai/core 0.1.9-beta.7 → 0.1.9

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 (165) hide show
  1. package/LICENSE +89 -0
  2. package/README.md +3 -1
  3. package/dist/constants/chatter-message.constants.d.ts +6 -0
  4. package/dist/constants/chatter-message.constants.d.ts.map +1 -1
  5. package/dist/constants/chatter-message.constants.js +7 -1
  6. package/dist/constants/chatter-message.constants.js.map +1 -1
  7. package/dist/controllers/authentication.controller.d.ts +12 -0
  8. package/dist/controllers/authentication.controller.d.ts.map +1 -1
  9. package/dist/controllers/authentication.controller.js +13 -0
  10. package/dist/controllers/authentication.controller.js.map +1 -1
  11. package/dist/controllers/chatter-message.controller.d.ts +1 -0
  12. package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
  13. package/dist/controllers/chatter-message.controller.js +12 -0
  14. package/dist/controllers/chatter-message.controller.js.map +1 -1
  15. package/dist/controllers/facebook-authentication.controller.d.ts +27 -0
  16. package/dist/controllers/facebook-authentication.controller.d.ts.map +1 -0
  17. package/dist/controllers/facebook-authentication.controller.js +117 -0
  18. package/dist/controllers/facebook-authentication.controller.js.map +1 -0
  19. package/dist/controllers/menu-item-metadata.controller.d.ts +1 -0
  20. package/dist/controllers/menu-item-metadata.controller.d.ts.map +1 -1
  21. package/dist/controllers/menu-item-metadata.controller.js +15 -0
  22. package/dist/controllers/menu-item-metadata.controller.js.map +1 -1
  23. package/dist/controllers/microsoft-authentication.controller.d.ts +27 -0
  24. package/dist/controllers/microsoft-authentication.controller.d.ts.map +1 -0
  25. package/dist/controllers/microsoft-authentication.controller.js +118 -0
  26. package/dist/controllers/microsoft-authentication.controller.js.map +1 -0
  27. package/dist/controllers/setting.controller.d.ts +2 -2
  28. package/dist/controllers/setting.controller.js +2 -2
  29. package/dist/decorators/auth.decorator.d.ts.map +1 -1
  30. package/dist/decorators/computed-field-provider.decorator.d.ts.map +1 -1
  31. package/dist/decorators/dashboard-question-data-provider.decorator.d.ts.map +1 -1
  32. package/dist/decorators/dashboard-selection-provider.decorator.d.ts.map +1 -1
  33. package/dist/decorators/disallow-in-production.decorator.d.ts.map +1 -1
  34. package/dist/decorators/error-codes-provider.decorator.d.ts.map +1 -1
  35. package/dist/decorators/extension-user-creation-provider.decorator.d.ts.map +1 -1
  36. package/dist/decorators/is-not-in-enum.decorator.d.ts.map +1 -1
  37. package/dist/decorators/mail-provider.decorator.d.ts.map +1 -1
  38. package/dist/decorators/roles.decorator.d.ts.map +1 -1
  39. package/dist/decorators/scheduled-job-provider.decorator.d.ts.map +1 -1
  40. package/dist/decorators/security-rule-config-provider.decorator.d.ts.map +1 -1
  41. package/dist/decorators/selection-provider.decorator.d.ts.map +1 -1
  42. package/dist/decorators/sms-provider.decorator.d.ts.map +1 -1
  43. package/dist/decorators/solid-database-module.decorator.d.ts.map +1 -1
  44. package/dist/decorators/whatsapp-provider.decorator.d.ts.map +1 -1
  45. package/dist/dtos/create-chatter-message.dto.d.ts +1 -0
  46. package/dist/dtos/create-chatter-message.dto.d.ts.map +1 -1
  47. package/dist/dtos/create-chatter-message.dto.js +7 -1
  48. package/dist/dtos/create-chatter-message.dto.js.map +1 -1
  49. package/dist/dtos/post-chatter-message.dto.d.ts +1 -0
  50. package/dist/dtos/post-chatter-message.dto.d.ts.map +1 -1
  51. package/dist/dtos/post-chatter-message.dto.js +6 -1
  52. package/dist/dtos/post-chatter-message.dto.js.map +1 -1
  53. package/dist/dtos/update-chatter-message.dto.d.ts +1 -0
  54. package/dist/dtos/update-chatter-message.dto.d.ts.map +1 -1
  55. package/dist/dtos/update-chatter-message.dto.js +7 -1
  56. package/dist/dtos/update-chatter-message.dto.js.map +1 -1
  57. package/dist/entities/chatter-message.entity.d.ts +1 -0
  58. package/dist/entities/chatter-message.entity.d.ts.map +1 -1
  59. package/dist/entities/chatter-message.entity.js +5 -1
  60. package/dist/entities/chatter-message.entity.js.map +1 -1
  61. package/dist/entities/user.entity.d.ts +8 -0
  62. package/dist/entities/user.entity.d.ts.map +1 -1
  63. package/dist/entities/user.entity.js +33 -1
  64. package/dist/entities/user.entity.js.map +1 -1
  65. package/dist/helpers/cors.helper.js +1 -1
  66. package/dist/helpers/cors.helper.js.map +1 -1
  67. package/dist/helpers/facebook-oauth.helper.d.ts +8 -0
  68. package/dist/helpers/facebook-oauth.helper.d.ts.map +1 -0
  69. package/dist/helpers/facebook-oauth.helper.js +11 -0
  70. package/dist/helpers/facebook-oauth.helper.js.map +1 -0
  71. package/dist/helpers/microsoft-oauth.helper.d.ts +9 -0
  72. package/dist/helpers/microsoft-oauth.helper.d.ts.map +1 -0
  73. package/dist/helpers/microsoft-oauth.helper.js +12 -0
  74. package/dist/helpers/microsoft-oauth.helper.js.map +1 -0
  75. package/dist/helpers/security.helper.d.ts.map +1 -1
  76. package/dist/helpers/string.helper.d.ts.map +1 -1
  77. package/dist/helpers/user-helper.d.ts.map +1 -1
  78. package/dist/helpers/user-helper.js +4 -0
  79. package/dist/helpers/user-helper.js.map +1 -1
  80. package/dist/index.d.ts +2 -0
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +2 -0
  83. package/dist/index.js.map +1 -1
  84. package/dist/interfaces.d.ts +19 -0
  85. package/dist/interfaces.d.ts.map +1 -1
  86. package/dist/interfaces.js.map +1 -1
  87. package/dist/passport-strategies/facebook-oauth.strategy.d.ts +16 -0
  88. package/dist/passport-strategies/facebook-oauth.strategy.d.ts.map +1 -0
  89. package/dist/passport-strategies/facebook-oauth.strategy.js +96 -0
  90. package/dist/passport-strategies/facebook-oauth.strategy.js.map +1 -0
  91. package/dist/passport-strategies/microsoft-oauth.strategy.d.ts +14 -0
  92. package/dist/passport-strategies/microsoft-oauth.strategy.d.ts.map +1 -0
  93. package/dist/passport-strategies/microsoft-oauth.strategy.js +77 -0
  94. package/dist/passport-strategies/microsoft-oauth.strategy.js.map +1 -0
  95. package/dist/seeders/seed-data/solid-core-metadata.json +27 -58
  96. package/dist/services/api-key.service.d.ts +17 -1
  97. package/dist/services/api-key.service.d.ts.map +1 -1
  98. package/dist/services/api-key.service.js +38 -2
  99. package/dist/services/api-key.service.js.map +1 -1
  100. package/dist/services/authentication.service.d.ts +61 -27
  101. package/dist/services/authentication.service.d.ts.map +1 -1
  102. package/dist/services/authentication.service.js +356 -164
  103. package/dist/services/authentication.service.js.map +1 -1
  104. package/dist/services/chatter-message.service.d.ts +1 -0
  105. package/dist/services/chatter-message.service.d.ts.map +1 -1
  106. package/dist/services/chatter-message.service.js +24 -7
  107. package/dist/services/chatter-message.service.js.map +1 -1
  108. package/dist/services/crud-helper.service.d.ts.map +1 -1
  109. package/dist/services/model-metadata.service.js +1 -1
  110. package/dist/services/model-metadata.service.js.map +1 -1
  111. package/dist/services/setting.service.d.ts +5 -2
  112. package/dist/services/setting.service.d.ts.map +1 -1
  113. package/dist/services/setting.service.js +51 -6
  114. package/dist/services/setting.service.js.map +1 -1
  115. package/dist/services/settings/default-settings-provider.service.d.ts +846 -0
  116. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  117. package/dist/services/settings/default-settings-provider.service.js +1096 -117
  118. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  119. package/dist/services/user.service.d.ts +12 -8
  120. package/dist/services/user.service.d.ts.map +1 -1
  121. package/dist/services/user.service.js +143 -32
  122. package/dist/services/user.service.js.map +1 -1
  123. package/dist/solid-core.module.d.ts.map +1 -1
  124. package/dist/solid-core.module.js +11 -3
  125. package/dist/solid-core.module.js.map +1 -1
  126. package/dist/transformers/array-transformer.d.ts.map +1 -1
  127. package/dist/transformers/boolean-transformer.d.ts.map +1 -1
  128. package/dist/transformers/datetime-transformer.d.ts.map +1 -1
  129. package/dist/transformers/integer-transformer.d.ts.map +1 -1
  130. package/dist/validators/is-parsable-int.d.ts.map +1 -1
  131. package/nest +0 -0
  132. package/package.json +8 -2
  133. package/src/constants/chatter-message.constants.ts +7 -0
  134. package/src/controllers/authentication.controller.ts +8 -1
  135. package/src/controllers/chatter-message.controller.ts +6 -0
  136. package/src/controllers/facebook-authentication.controller.ts +113 -0
  137. package/src/controllers/menu-item-metadata.controller.ts +21 -15
  138. package/src/controllers/microsoft-authentication.controller.ts +116 -0
  139. package/src/dtos/create-chatter-message.dto.ts +11 -0
  140. package/src/dtos/post-chatter-message.dto.ts +4 -0
  141. package/src/dtos/update-chatter-message.dto.ts +13 -1
  142. package/src/entities/chatter-message.entity.ts +4 -1
  143. package/src/entities/user.entity.ts +32 -0
  144. package/src/helpers/cors.helper.ts +1 -1
  145. package/src/helpers/facebook-oauth.helper.ts +17 -0
  146. package/src/helpers/microsoft-oauth.helper.ts +19 -0
  147. package/src/helpers/user-helper.ts +4 -0
  148. package/src/index.ts +2 -0
  149. package/src/interfaces.ts +32 -1
  150. package/src/passport-strategies/facebook-oauth.strategy.ts +115 -0
  151. package/src/passport-strategies/microsoft-oauth.strategy.ts +70 -0
  152. package/src/seeders/seed-data/solid-core-metadata.json +27 -58
  153. package/src/services/api-key.service.ts +77 -35
  154. package/src/services/authentication.service.ts +1947 -1432
  155. package/src/services/chatter-message.service.ts +23 -3
  156. package/src/services/model-metadata.service.ts +1 -1
  157. package/src/services/setting.service.ts +64 -8
  158. package/src/services/settings/default-settings-provider.service.ts +1168 -156
  159. package/src/services/user.service.ts +220 -61
  160. package/src/solid-core.module.ts +25 -8
  161. package/dev-grooming-docs/ozzy-prompts.txt +0 -70
  162. package/docs/grouping-enhancements.md +0 -89
  163. package/docs/seed-changes.md +0 -65
  164. package/docs/test-data-workflow.md +0 -200
  165. package/docs/type-declaration-import-issue.md +0 -24
@@ -1,1463 +1,1978 @@
1
- import { HttpService } from '@nestjs/axios';
1
+ import { HttpService } from "@nestjs/axios";
2
2
  import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service";
3
3
  import {
4
- BadRequestException,
5
- ConflictException,
6
- ForbiddenException,
7
- Injectable,
8
- InternalServerErrorException,
9
- Logger,
10
- NotFoundException,
11
- UnauthorizedException,
12
- } from '@nestjs/common';
13
- import { EventEmitter2 } from '@nestjs/event-emitter';
14
- import { JwtService } from '@nestjs/jwt';
15
- import { InjectDataSource } from '@nestjs/typeorm';
16
- import { isEmpty, isNotEmpty } from 'class-validator';
17
- import { randomInt, randomUUID } from 'crypto';
18
- import { ERROR_MESSAGES } from 'src/constants/error-messages';
19
- import { SUCCESS_MESSAGES } from 'src/constants/success-messages';
20
- import { CreateUserDto } from 'src/dtos/create-user.dto';
21
- import { MailFactory } from 'src/factories/mail.factory';
22
- import { UserRepository } from 'src/repository/user.repository';
23
- import { DataSource, Repository } from 'typeorm';
24
- import { v4 as uuidv4 } from 'uuid';
4
+ BadRequestException,
5
+ ConflictException,
6
+ ForbiddenException,
7
+ Injectable,
8
+ InternalServerErrorException,
9
+ Logger,
10
+ NotFoundException,
11
+ UnauthorizedException,
12
+ } from "@nestjs/common";
13
+ import { EventEmitter2 } from "@nestjs/event-emitter";
14
+ import { JwtService } from "@nestjs/jwt";
15
+ import { InjectDataSource } from "@nestjs/typeorm";
16
+ import { isEmpty, isNotEmpty } from "class-validator";
17
+ import { randomInt, randomUUID } from "crypto";
18
+ import { ERROR_MESSAGES } from "src/constants/error-messages";
19
+ import { SUCCESS_MESSAGES } from "src/constants/success-messages";
20
+ import { CreateUserDto } from "src/dtos/create-user.dto";
21
+ import { MailFactory } from "src/factories/mail.factory";
22
+ import { UserRepository } from "src/repository/user.repository";
23
+ import { DataSource, DeepPartial, Repository } from "typeorm";
24
+ import { v4 as uuidv4 } from "uuid";
25
25
  import {
26
- ForgotPasswordSendVerificationTokenOn,
27
- PasswordlessLoginValidateWhatSources,
28
- PasswordlessRegistrationValidateWhatSources
26
+ ForgotPasswordSendVerificationTokenOn,
27
+ PasswordlessLoginValidateWhatSources,
28
+ PasswordlessRegistrationValidateWhatSources,
29
29
  } from "../constants";
30
30
  import { ChangePasswordDto } from "../dtos/change-password.dto";
31
- import { ConfirmForgotPasswordDto } from '../dtos/confirm-forgot-password.dto';
32
- import { InitiateForgotPasswordDto } from '../dtos/initiate-forgot-password.dto';
33
- import { OTPConfirmOTPDto } from '../dtos/otp-confirm-otp.dto';
34
- import { OTPSignInDto } from '../dtos/otp-sign-in.dto';
35
- import { OTPSignUpDto } from '../dtos/otp-sign-up.dto';
36
- import { RefreshTokenDto } from '../dtos/refresh-token.dto';
37
- import { SignInDto } from '../dtos/sign-in.dto';
38
- import { SignUpDto } from '../dtos/sign-up.dto';
39
- import { User } from '../entities/user.entity';
31
+ import { ConfirmForgotPasswordDto } from "../dtos/confirm-forgot-password.dto";
32
+ import { InitiateForgotPasswordDto } from "../dtos/initiate-forgot-password.dto";
33
+ import { OTPConfirmOTPDto } from "../dtos/otp-confirm-otp.dto";
34
+ import { OTPSignInDto } from "../dtos/otp-sign-in.dto";
35
+ import { OTPSignUpDto } from "../dtos/otp-sign-up.dto";
36
+ import { RefreshTokenDto } from "../dtos/refresh-token.dto";
37
+ import { SignInDto } from "../dtos/sign-in.dto";
38
+ import { SignUpDto } from "../dtos/sign-up.dto";
39
+ import { User } from "../entities/user.entity";
40
40
  import { EventDetails, EventType } from "../interfaces";
41
- import { ActiveUserData } from '../interfaces/active-user-data.interface';
42
- import { HashingService } from './hashing.service';
43
- import { InvalidatedRefreshTokenError, RefreshTokenIdsStorageService } from './refresh-token-ids-storage.service';
44
- import { SsoCodeStorageService } from './sso-code-storage.service';
45
- import { RoleMetadataService } from './role-metadata.service';
46
- import { SettingService } from './setting.service';
47
- import { UserActivityHistoryService } from './user-activity-history.service';
48
- import { UserService } from './user.service';
49
- import { SmsFactory } from 'src/factories/sms.factory';
50
- import { SolidRegistry } from 'src/helpers/solid-registry';
41
+ import { ActiveUserData } from "../interfaces/active-user-data.interface";
42
+ import { HashingService } from "./hashing.service";
43
+ import {
44
+ InvalidatedRefreshTokenError,
45
+ RefreshTokenIdsStorageService,
46
+ } from "./refresh-token-ids-storage.service";
47
+ import { SsoCodeStorageService } from "./sso-code-storage.service";
48
+ import { RoleMetadataService } from "./role-metadata.service";
49
+ import { SettingService } from "./setting.service";
50
+ import { UserActivityHistoryService } from "./user-activity-history.service";
51
+ import { UserService } from "./user.service";
52
+ import { SmsFactory } from "src/factories/sms.factory";
53
+ import { SolidRegistry } from "src/helpers/solid-registry";
51
54
 
52
55
  enum LoginProvider {
53
- LOCAL = 'local',
54
- GOOGLE = 'google',
55
- OTP = 'otp',
56
+ LOCAL = "local",
57
+ GOOGLE = "google",
58
+ OTP = "otp",
56
59
  }
57
60
 
58
61
  interface otp {
59
- token: string;
60
- expiresAt: Date;
62
+ token: string;
63
+ expiresAt: Date;
61
64
  }
62
65
 
63
66
  @Injectable()
64
67
  export class AuthenticationService {
65
- private readonly logger = new Logger(AuthenticationService.name);
66
- // private readonly mailService: IMail;
67
- constructor(
68
- private readonly userService: UserService,
69
- // @InjectRepository(User) private readonly userRepository: Repository<User>,
70
- private readonly userRepository: UserRepository,
71
- private readonly hashingService: HashingService,
72
- private readonly jwtService: JwtService,
73
- private readonly refreshTokenIdsStorage: RefreshTokenIdsStorageService,
74
- private readonly httpService: HttpService,
75
- // private readonly mailService: SMTPEMailService,
76
- private readonly mailServiceFactory: MailFactory,
77
- // private readonly smsService: Msg91OTPService,
78
- private readonly smsFactory: SmsFactory,
79
- private readonly eventEmitter: EventEmitter2,
80
- private readonly settingService: SettingService,
81
- private readonly roleMetadataService: RoleMetadataService,
82
- private readonly userActivityHistoryService: UserActivityHistoryService,
83
- private readonly ssoCodeStorage: SsoCodeStorageService,
84
-
85
- @InjectDataSource()
86
- private readonly dataSource: DataSource,
87
- private readonly solidRegistry: SolidRegistry,
68
+ private readonly logger = new Logger(AuthenticationService.name);
69
+ // private readonly mailService: IMail;
70
+ constructor(
71
+ private readonly userService: UserService,
72
+ // @InjectRepository(User) private readonly userRepository: Repository<User>,
73
+ private readonly userRepository: UserRepository,
74
+ private readonly hashingService: HashingService,
75
+ private readonly jwtService: JwtService,
76
+ private readonly refreshTokenIdsStorage: RefreshTokenIdsStorageService,
77
+ private readonly httpService: HttpService,
78
+ // private readonly mailService: SMTPEMailService,
79
+ private readonly mailServiceFactory: MailFactory,
80
+ // private readonly smsService: Msg91OTPService,
81
+ private readonly smsFactory: SmsFactory,
82
+ private readonly eventEmitter: EventEmitter2,
83
+ private readonly settingService: SettingService,
84
+ private readonly roleMetadataService: RoleMetadataService,
85
+ private readonly userActivityHistoryService: UserActivityHistoryService,
86
+ private readonly ssoCodeStorage: SsoCodeStorageService,
87
+
88
+ @InjectDataSource()
89
+ private readonly dataSource: DataSource,
90
+ private readonly solidRegistry: SolidRegistry,
91
+ ) {
92
+ // this.mailService = this.mailServiceFactory.getMailService();
93
+ }
94
+
95
+ private async getCompanyLogo(): Promise<string> {
96
+ return this.settingService.getConfigValue<SolidCoreSetting>("companylogo");
97
+ }
98
+
99
+ async resolveUser(username: string, email: string) {
100
+ return await this.userRepository.findOne({
101
+ where: [{ username: username }, { email: email }],
102
+ relations: {
103
+ roles: true,
104
+ },
105
+ });
106
+ }
107
+
108
+ async updatePasswordDetails(user: User, newPassword: string) {
109
+ user.password = await this.hashingService.hash(newPassword);
110
+ user.passwordScheme = this.hashingService.name();
111
+ user.passwordSchemeVersion = this.hashingService.currentVersion();
112
+ user.rehashedAt = new Date();
113
+ await this.userRepository.update(user.id, {
114
+ password: user.password,
115
+ passwordScheme: user.passwordScheme,
116
+ passwordSchemeVersion: user.passwordSchemeVersion,
117
+ rehashedAt: user.rehashedAt,
118
+ });
119
+ return user;
120
+ }
121
+
122
+ async resolveUserByVerificationToken(token: string) {
123
+ return await this.userRepository.findOne({
124
+ where: { verificationTokenOnForgotPassword: token },
125
+ relations: { roles: true },
126
+ });
127
+ }
128
+
129
+ private async validateUserForPasswordLogin(
130
+ user: User,
131
+ password: string,
132
+ ): Promise<void> {
133
+ if (!user.active) {
134
+ throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_ACTIVE);
135
+ }
136
+ this.checkAccountBlocked(user);
137
+ const isEqual = await this.hashingService.compare(
138
+ password,
139
+ user.password,
140
+ user.passwordSchemeVersion,
141
+ );
142
+ if (!isEqual) {
143
+ await this.incrementFailedAttempts(user);
144
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
145
+ }
146
+ }
147
+
148
+ private async rehashPasswordIfRequired(
149
+ user: User,
150
+ password: string,
151
+ ): Promise<void> {
152
+ if (
153
+ this.hashingService.needsRehash(user.password, user.passwordSchemeVersion)
88
154
  ) {
89
- // this.mailService = this.mailServiceFactory.getMailService();
90
- }
91
-
92
- private async getCompanyLogo(): Promise<string> {
93
- return this.settingService.getConfigValue<SolidCoreSetting>('companylogo');
94
- }
95
-
96
- async resolveUser(username: string, email: string) {
97
- return await this.userRepository.findOne({
98
- where: [
99
- { username: username },
100
- { email: email },
101
- ],
102
- relations: {
103
- roles: true
104
- }
105
- });
106
- }
107
-
108
- async updatePasswordDetails(user: User, newPassword: string) {
109
- user.password = await this.hashingService.hash(newPassword);
110
- user.passwordScheme = this.hashingService.name();
111
- user.passwordSchemeVersion = this.hashingService.currentVersion();
112
- user.rehashedAt = new Date();
113
- await this.userRepository.update(user.id, {
114
- password: user.password,
115
- passwordScheme: user.passwordScheme,
116
- passwordSchemeVersion: user.passwordSchemeVersion,
117
- rehashedAt: user.rehashedAt
118
- });
119
- return user;
120
- }
121
-
122
- async resolveUserByVerificationToken(token: string) {
123
- return await this.userRepository.findOne({
124
- where: { verificationTokenOnForgotPassword: token },
125
- relations: { roles: true }
126
- });
127
- }
128
-
129
- private async validateUserForPasswordLogin(user: User, password: string): Promise<void> {
130
- if (!user.active) {
131
- throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_ACTIVE);
132
- }
133
- this.checkAccountBlocked(user);
134
- const isEqual = await this.hashingService.compare(password, user.password, user.passwordSchemeVersion);
135
- if (!isEqual) {
136
- await this.incrementFailedAttempts(user);
137
- throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
138
- }
139
- }
140
-
141
- private async rehashPasswordIfRequired(user: User, password: string): Promise<void> {
142
- if (this.hashingService.needsRehash(user.password, user.passwordSchemeVersion)) {
143
- await this.updatePasswordDetails(user, password);
144
- }
145
- }
146
-
147
- private static readonly SIGNUP_DTO_KEYS = new Set(['username', 'email', 'password', 'fullName', 'mobile', 'roles', 'forcePasswordChange']);
148
-
149
- async signUp(signUpDto: SignUpDto & Record<string, any>, activeUser: ActiveUserData = null): Promise<User> {
150
- const hasExtensionFields = Object.keys(signUpDto).some(k => !AuthenticationService.SIGNUP_DTO_KEYS.has(k));
151
- if (hasExtensionFields) {
152
- const provider = this.solidRegistry.getExtensionUserCreationProvider();
153
- if (!provider) {
154
- throw new InternalServerErrorException(
155
- 'No ExtensionUserCreationProvider registered. Register one to handle extension user creation.',
156
- );
157
- }
158
- const entity = await provider.buildExtensionEntity(signUpDto);
159
- const effectiveDto = { ...signUpDto, roles: provider.roles(signUpDto) };
160
- return this.performSignUp(effectiveDto, entity, provider.repo as Repository<User>);
161
- }
162
- return this.performSignUp(signUpDto, new User(), this.userRepository);
163
- }
164
-
165
- private async performSignUp<T extends User>(signUpDto: SignUpDto, entity: T, repo: Repository<T>): Promise<T> {
166
- try {
167
- const onForcePasswordChange = this.settingService.getConfigValue<SolidCoreSetting>('forceChangePasswordOnFirstLogin');
168
- const activateUserOnRegistration = this.settingService.getConfigValue<SolidCoreSetting>('activateUserOnRegistration');
169
- const defaultRole = this.settingService.getConfigValue<SolidCoreSetting>('defaultRole');
170
-
171
- var { user, pwd, autoGeneratedPwd } = await this.populateForSignup<T>(entity, signUpDto, activateUserOnRegistration, onForcePasswordChange);
172
- const privateDto = signUpDto as { isAllowedToGenerateApiKeys?: boolean };
173
- if (privateDto.isAllowedToGenerateApiKeys !== undefined) {
174
- user.isAllowedToGenerateApiKeys = privateDto.isAllowedToGenerateApiKeys;
175
- }
176
- const savedUser = await repo.save(user);
177
- const userRoles = signUpDto.roles ?? [];
178
- if ((signUpDto.roles?.length ?? 0) === 0 && signUpDto.username !== 'sa' && defaultRole) {
179
- userRoles.push(defaultRole);
180
- }
181
- await this.handlePostSignup(savedUser, userRoles, pwd, autoGeneratedPwd);
182
-
183
- return savedUser;
184
- } catch (err) {
185
- const pgUniqueViolationErrorCode = '23505';
186
- if (err.code === pgUniqueViolationErrorCode) {
187
- throw new ConflictException(parseUniqueConstraintError(err.detail || ERROR_MESSAGES.UNIQUE_CONSTRAINT_VIOLATION));
188
- }
189
- throw err;
190
- }
191
- }
192
-
193
- /** @deprecated Use IExtensionUserCreationProvider instead. Kept for backward compatibility. */
194
- async signupForExtensionUser<T extends User, U extends CreateUserDto>(signUpDto: SignUpDto, extensionUserDto: U, extensionUserRepo: Repository<T>): Promise<T> {
195
- // @ts-ignore
196
- const entity = extensionUserRepo.merge(extensionUserRepo.create() as T, extensionUserDto);
197
- return this.performSignUp(signUpDto, entity, extensionUserRepo);
198
- }
199
-
200
-
201
- private async populateForSignup<T extends User>(user: T, signUpDto: SignUpDto, isUserActive: boolean = true, onForcePasswordChange?: boolean) {
202
- // const user = new User();
203
- let autoGeneratedPwdPermission = this.settingService.getConfigValue<SolidCoreSetting>('iamAutoGeneratedPassword');
204
- if (signUpDto.roles && signUpDto.roles.length > 0) {
205
- for (let i = 0; i < signUpDto.roles.length; i++) {
206
- const roleName = signUpDto.roles[i];
207
- await this.roleMetadataService.findRoleByName(roleName);
208
- }
209
- }
210
- user.username = signUpDto.username;
211
- user.email = signUpDto.email;
212
- user.fullName = signUpDto.fullName;
213
- user.forcePasswordChange = onForcePasswordChange;
214
- if (signUpDto.mobile) {
215
- user.mobile = signUpDto.mobile;
216
- }
217
- // this.logger.debug("user", user);
218
-
219
- // If password has been specified by the user, then we simply create & activate the user based on the configuration parameter "activateUserOnRegistration".
220
- let pwd = '';
221
- let autoGeneratedPwd = '';
222
-
223
- // User has specified password
224
- if (signUpDto.password) {
225
- pwd = await this.hashingService.hash(signUpDto.password);
226
- }
227
- // User has not specified password
228
- else {
229
- // When user does not specify password, and system is configured to auto generate passwords.
230
- if (autoGeneratedPwdPermission?.toString().toLowerCase() === 'true') {
231
- autoGeneratedPwd = this.generatePassword();
232
- pwd = await this.hashingService.hash(autoGeneratedPwd);
233
- user.forcePasswordChange = true;
234
- }
235
- // When user does not specify password, and system is not configured to auto generate passwords.
236
- else {
237
- // This means that most likely the system is going to be using password-less login.
238
- // If that is not the case then we can raise a bad request exception...
239
- if (!await this.isPasswordlessRegistrationEnabled()) {
240
- this.logger.error('User being created without password, and password less login is also not enabled in the system. Is this intentional?');
241
- throw new BadRequestException(ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED);
242
- }
243
-
244
- // Save the hash of the blank password, anyways since passwordless login is enabled it does not matter.
245
- pwd = await this.hashingService.hash(pwd);
246
- }
247
- }
248
-
249
- user.password = pwd;
250
- user.passwordScheme = this.hashingService.name(); // e.g. bcrypt
251
- user.passwordSchemeVersion = this.hashingService.currentVersion(); // e.g. 1, 2, 3 ...
252
- user.active = isUserActive;
253
- return { user, pwd, autoGeneratedPwd };
254
- }
255
-
256
-
257
- private async handlePostSignup(user: User, roles: string[] = [], pwd: string, autoGeneratedPwd: string) {
258
- await this.userService.initializeRolesForNewUser(roles, user);
259
-
260
- // if forcePasswordChange is true, then we trigger an email to the user to change the password, this needs to be done using a queue.
261
- // Create a new method like notifyUserOnForcePasswordChange, create a new email template we can call it on-force-password-change this template to include the random password
262
- if (user.forcePasswordChange && autoGeneratedPwd) {
263
- await this.notifyUserOnForcePasswordChange(user, autoGeneratedPwd);
264
- }
265
-
266
- // Send welcome notifications (email/SMS) if enabled.
267
- await this.notifyUserOnSignup(user);
268
- }
269
-
270
-
271
- generatePassword(length: number = 8): string {
272
- const upperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
273
- const lowerCase = "abcdefghijklmnopqrstuvwxyz";
274
- const numbers = "0123456789";
275
- const specialChars = "@$#";
276
- const allChars = upperCase + lowerCase + numbers + specialChars;
277
-
278
- let password = "";
279
-
280
- for (let i = 0; i < length; i++) {
281
- const randomIndex = Math.floor(Math.random() * allChars.length);
282
- password += allChars[randomIndex];
283
- }
284
-
285
- return password;
286
- }
287
-
288
- private async notifyUserOnForcePasswordChange(user: User, autoGeneratedPwd: string) {
289
- const companyLogo = await this.getCompanyLogo();
290
- const mailService = this.mailServiceFactory.getMailService();
291
- mailService.sendEmailUsingTemplate(
292
- user.email,
293
- 'on-force-password-change',
294
- {
295
- solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
296
- solidAppWebsiteUrl: this.settingService.getConfigValue<SolidCoreSetting>('solidAppWebsiteUrl'),
297
- frontendLoginPageUrl: this.settingService.getConfigValue<SolidCoreSetting>('frontendLoginPageUrl'),
298
- email: user.email,
299
- fullName: user.fullName,
300
- userName: user.username,
301
- password: autoGeneratedPwd,
302
- companyLogoUrl: companyLogo
303
- },
304
- this.settingService.getConfigValue<SolidCoreSetting>('shouldQueueEmails'),
305
- null,
306
- null,
307
- 'user',
308
- user.id
155
+ await this.updatePasswordDetails(user, password);
156
+ }
157
+ }
158
+
159
+ private static readonly SIGNUP_DTO_KEYS = new Set([
160
+ "username",
161
+ "email",
162
+ "password",
163
+ "fullName",
164
+ "mobile",
165
+ "roles",
166
+ "forcePasswordChange",
167
+ ]);
168
+
169
+ async signUp(
170
+ signUpDto: SignUpDto & Record<string, any>,
171
+ activeUser: ActiveUserData = null,
172
+ ): Promise<User> {
173
+ const hasExtensionFields = Object.keys(signUpDto).some(
174
+ (k) => !AuthenticationService.SIGNUP_DTO_KEYS.has(k),
175
+ );
176
+ if (hasExtensionFields) {
177
+ const provider = this.solidRegistry.getExtensionUserCreationProvider();
178
+ if (!provider) {
179
+ throw new InternalServerErrorException(
180
+ "No ExtensionUserCreationProvider registered. Register one to handle extension user creation.",
309
181
  );
310
-
311
- }
312
-
313
- private async isWelcomeEmailEnabled(): Promise<boolean> {
314
- const sendWelcomeEmailOnSignup = this.settingService.getConfigValue<SolidCoreSetting>('sendWelcomeEmailOnSignup');
315
- return sendWelcomeEmailOnSignup;
316
- }
317
-
318
- private async isWelcomeSmsEnabled(): Promise<boolean> {
319
- const sendWelcomeSmsOnSignup = this.settingService.getConfigValue<SolidCoreSetting>('sendWelcomeSmsOnSignup');
320
- return sendWelcomeSmsOnSignup;
321
- }
322
-
323
- private async notifyUserOnSignup(user: User) {
324
- const companyLogo = await this.getCompanyLogo();
325
- // Email welcome
326
- if (await this.isWelcomeEmailEnabled()) {
327
- const mailService = this.mailServiceFactory.getMailService();
328
- mailService.sendEmailUsingTemplate(
329
- user.email,
330
- 'email-on-signup',
331
- {
332
- solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
333
- solidAppWebsiteUrl: this.settingService.getConfigValue<SolidCoreSetting>('solidAppWebsiteUrl'),
334
- frontendLoginPageUrl: this.settingService.getConfigValue<SolidCoreSetting>('frontendLoginPageUrl'),
335
- email: user.email,
336
- fullName: user.fullName,
337
- userName: user.username,
338
- companyLogoUrl: companyLogo
339
- },
340
- this.settingService.getConfigValue<SolidCoreSetting>('shouldQueueEmails'),
341
- null,
342
- null,
343
- 'user',
344
- user.id
345
- );
346
- }
347
-
348
- // SMS welcome
349
- const isWelcomeSmsEnabled = await this.isWelcomeSmsEnabled()
350
- if (isWelcomeSmsEnabled && user.mobile) {
351
- const smsService = this.smsFactory.getSmsService();
352
- smsService.sendSMSUsingTemplate(
353
- user.mobile,
354
- 'text-on-signup',
355
- {
356
- solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
357
- frontendLoginPageUrl: this.settingService.getConfigValue<SolidCoreSetting>('frontendLoginPageUrl'),
358
- firstName: user.username,
359
- fullName: user.fullName ? user.fullName : user.username
360
- },
361
- this.settingService.getConfigValue<SolidCoreSetting>('shouldQueueSms'),
362
-
363
- );
364
- }
365
- }
366
-
367
- async otpInitiateRegistration(signUpDto: OTPSignUpDto) {
368
- const isPasswordlessRegistrationEnabled = await this.isPasswordlessRegistrationEnabled();
369
- if (!isPasswordlessRegistrationEnabled) {
370
- throw new BadRequestException(ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED);
371
- }
372
-
373
- const validationSource = this.resolvePasswordlessValidationSource();
374
- this.validateOtpRegistrationInput(signUpDto, validationSource);
375
-
376
- const existingUser = await this.findExistingRegistrationUser(signUpDto);
377
- if (isNotEmpty(existingUser) && existingUser.active) {
378
- throw new ConflictException(ERROR_MESSAGES.USER_ALREADY_EXISTS);
379
- }
380
-
381
- try {
382
- const user = await this.upsertUserWithRegistrationVerificationTokens(existingUser, signUpDto, validationSource);
383
- await this.notifyUserOnOtpInitiateRegistration(user, validationSource);
384
- } catch (err) {
385
- if (err.code === '23505') {
386
- throw new ConflictException(ERROR_MESSAGES.USER_ALREADY_EXISTS);
387
- }
388
- throw err;
389
- }
390
-
391
- return { message: SUCCESS_MESSAGES.OTP_SENT_SUCCESS_REGISTRATION };
392
- }
393
-
394
- private validateOtpRegistrationInput(signUpDto: OTPSignUpDto, validationSource: string): void {
395
- if (validationSource === PasswordlessRegistrationValidateWhatSources.EMAIL && isEmpty(signUpDto.email)) {
396
- throw new BadRequestException(ERROR_MESSAGES.EMAIL_REQUIRED_FOR_VALIDATION);
397
- }
398
- if (validationSource === PasswordlessRegistrationValidateWhatSources.MOBILE && isEmpty(signUpDto.mobile)) {
399
- throw new BadRequestException(ERROR_MESSAGES.MOBILE_REQUIRED_FOR_VALIDATION);
400
- }
401
- }
402
-
403
- private async findExistingRegistrationUser(signUpDto: OTPSignUpDto): Promise<User> {
404
- return this.userRepository.findOne({ //TODO Perhaps we should use the user service instead of the repository directly.
405
- where: [
406
- { email: signUpDto.email },
407
- { mobile: signUpDto.mobile },
408
- { username: signUpDto.username },
409
- ]
410
- });
411
- }
412
-
413
- private resolvePasswordlessValidationSource(): string {
414
- return this.settingService.getConfigValue<SolidCoreSetting>('passwordlessRegistrationValidateWhat');
415
- }
416
-
417
- private async upsertUserWithRegistrationVerificationTokens(existingUser: User, signUpDto: OTPSignUpDto, validationSource: string): Promise<User> {
418
- let user = existingUser;
419
- if (isEmpty(user)) {
420
- user = this.createUser(signUpDto);
421
- user.active = false; // User will be activated only after OTP verification, hence setting active to false for new user.
422
- await this.assignRegistrationOtp(validationSource, user);
423
- await this.userRepository.save(user);
424
- await this.userService.addRoleToUser(user.username, this.settingService.getConfigValue<SolidCoreSetting>('defaultRole'));
425
- } else {
426
- await this.assignRegistrationOtp(validationSource, user);
427
- await this.userRepository.save(user);
428
- }
429
- return user;
430
- }
431
-
432
- // Create a new user entity.
433
- private createUser(signUpDto: OTPSignUpDto) {
434
- const user = new User();
435
- user.username = signUpDto.username;
436
- user.email = signUpDto.email;
437
- user.mobile = signUpDto.mobile;
438
- user.customPayload = signUpDto.customPayload;
439
- user.lastLoginProvider = LoginProvider.OTP;
440
- return user;
441
- }
442
-
443
- // Generate the validation tokens for the user i.e (system configured + user provided)
444
- private async assignRegistrationOtp(passwordlessRegistrationValidateWhat: string, user: User) {
445
- if (!passwordlessRegistrationValidateWhat) {
446
- throw new BadRequestException(ERROR_MESSAGES.VALIDATION_SOURCE_REQUIRED);
447
- }
448
- const autoLoginUserOnRegistration = this.settingService.getConfigValue<SolidCoreSetting>('autoLoginUserOnRegistration');
449
- if (passwordlessRegistrationValidateWhat === PasswordlessRegistrationValidateWhatSources.EMAIL) {
450
- const { token, expiresAt } = await this.otp();
451
- user.emailVerificationTokenOnRegistration = token;
452
- user.emailVerificationTokenOnRegistrationExpiresAt = expiresAt;
453
- if (autoLoginUserOnRegistration) {
454
- user.emailVerificationTokenOnLogin = token;
455
- user.emailVerificationTokenOnLoginExpiresAt = expiresAt;
456
- }
457
- }
458
- if (passwordlessRegistrationValidateWhat === PasswordlessRegistrationValidateWhatSources.MOBILE) {
459
- const { token, expiresAt } = await this.otp();
460
- user.mobileVerificationTokenOnRegistration = token;
461
- user.mobileVerificationTokenOnRegistrationExpiresAt = expiresAt;
462
- if (autoLoginUserOnRegistration) {
463
- user.mobileVerificationTokenOnLogin = token;
464
- user.mobileVerificationTokenOnLoginExpiresAt = expiresAt;
465
- }
466
- }
467
- }
468
-
469
- private async notifyUserOnOtpInitiateRegistration(user: User, registrationValidationSource: string) {
470
- const companyLogo = await this.getCompanyLogo();
471
- if (registrationValidationSource === PasswordlessLoginValidateWhatSources.EMAIL) {
472
- const mailService = this.mailServiceFactory.getMailService();
473
- mailService.sendEmailUsingTemplate(
474
- user.email,
475
- 'otp-on-register',
476
- {
477
- solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
478
- solidAppWebsiteUrl: this.settingService.getConfigValue<SolidCoreSetting>('solidAppWebsiteUrl'),
479
- firstName: user.username,
480
- fullName: user.fullName ? user.fullName : user.username,
481
- emailVerificationTokenOnRegistration: user.emailVerificationTokenOnRegistration,
482
- companyLogoUrl: companyLogo
483
- },
484
- this.settingService.getConfigValue<SolidCoreSetting>('shouldQueueEmails'),
485
- null,
486
- null,
487
- 'user',
488
- user.id
489
- );
490
- }
491
- if (registrationValidationSource === PasswordlessLoginValidateWhatSources.MOBILE) {
492
- const smsService = this.smsFactory.getSmsService();
493
- smsService.sendSMSUsingTemplate(
494
- user.mobile,
495
- 'otp-on-register',
496
- {
497
- solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
498
- otp: user.mobileVerificationTokenOnRegistration,
499
- mobileVerificationTokenOnRegistration: user.mobileVerificationTokenOnRegistration,
500
- firstName: user.username,
501
- fullName: user.fullName ? user.fullName : user.username,
502
- companyLogoUrl: companyLogo
503
- },
504
- this.settingService.getConfigValue<SolidCoreSetting>('shouldQueueSms'),
505
-
506
- );
507
- }
508
- }
509
-
510
- async otpConfirmRegistration(confirmSignUpDto: OTPConfirmOTPDto) {
511
- const isPasswordlessRegistrationEnabled = await this.isPasswordlessRegistrationEnabled();
512
- if (!isPasswordlessRegistrationEnabled) {
513
- throw new BadRequestException(ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED);
514
- }
515
-
516
- const { type, identifier, otp } = confirmSignUpDto;
517
- if (type !== PasswordlessRegistrationValidateWhatSources.EMAIL &&
518
- type !== PasswordlessRegistrationValidateWhatSources.MOBILE) {
519
- throw new BadRequestException(ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
520
- }
521
-
522
- const user = await this.findUserByRegistrationIdentifier(type, identifier);
523
- this.validateRegistrationOtp(user, otp, type);
524
- this.clearRegistrationOtp(user, type);
525
- user.active = this.settingService.getConfigValue<SolidCoreSetting>('activateUserOnRegistration') &&
526
- await this.areAllPasswordlessRegistrationValidationSourcesVerified(user);
527
-
528
- const savedUser: User = await this.userRepository.save(user);
529
- this.triggerRegistrationEvent(savedUser);
530
- return { active: savedUser.active, message: `User registration verified for ${type}` };
531
- }
532
-
533
- private async findUserByRegistrationIdentifier(
534
- type: PasswordlessRegistrationValidateWhatSources,
535
- identifier: string,
536
- ): Promise<User> {
537
- const where = type === PasswordlessRegistrationValidateWhatSources.EMAIL
538
- ? { email: identifier }
539
- : { mobile: identifier };
540
- const user = await this.userRepository.findOne({ where });
541
- if (!user) {
542
- throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND);
543
- }
544
- return user;
545
- }
546
-
547
- private validateRegistrationOtp(
548
- user: User,
549
- otp: string,
550
- type: PasswordlessRegistrationValidateWhatSources,
551
- ): void {
552
- const isEmail = type === PasswordlessRegistrationValidateWhatSources.EMAIL;
553
- const token = isEmail ? user.emailVerificationTokenOnRegistration : user.mobileVerificationTokenOnRegistration;
554
- const expiresAt = isEmail ? user.emailVerificationTokenOnRegistrationExpiresAt : user.mobileVerificationTokenOnRegistrationExpiresAt;
555
-
556
- if (token !== otp) {
557
- throw new UnauthorizedException(ERROR_MESSAGES.INVALID_OTP);
558
- }
559
- if (expiresAt < new Date()) {
560
- throw new UnauthorizedException(ERROR_MESSAGES.OTP_EXPIRED);
561
- }
562
- }
563
-
564
- private clearRegistrationOtp(user: User, type: PasswordlessRegistrationValidateWhatSources): void {
565
- if (type === PasswordlessRegistrationValidateWhatSources.EMAIL) {
566
- user.emailVerifiedOnRegistrationAt = new Date();
567
- user.emailVerificationTokenOnRegistration = null;
568
- user.emailVerificationTokenOnRegistrationExpiresAt = null;
569
- } else {
570
- user.mobileVerifiedOnRegistrationAt = new Date();
571
- user.mobileVerificationTokenOnRegistration = null;
572
- user.mobileVerificationTokenOnRegistrationExpiresAt = null;
573
- }
574
- }
575
-
576
- private triggerRegistrationEvent(savedUser: User) {
577
- // Trigger events for user registration.
578
- const event = new EventDetails<User>(EventType.USER_REGISTERED, savedUser);
579
- this.eventEmitter.emit(EventType.USER_REGISTERED, event);
580
- }
581
-
582
- private async areAllPasswordlessRegistrationValidationSourcesVerified(user: User): Promise<boolean> {
583
- const registrationValidationSource = this.resolvePasswordlessValidationSource();
584
- if (registrationValidationSource === PasswordlessLoginValidateWhatSources.EMAIL) {
585
- if (!user.emailVerifiedOnRegistrationAt) {
586
- return false;
587
- }
588
- }
589
- if (registrationValidationSource === PasswordlessLoginValidateWhatSources.MOBILE) {
590
- if (!user.mobileVerifiedOnRegistrationAt) {
591
- return false;
592
- }
593
- }
594
- return true;
595
- }
596
-
597
- private async otp(): Promise<otp> {
598
- const now = new Date();
599
- const otpExpiry = this.settingService.getConfigValue<SolidCoreSetting>('otpExpiry');
600
- now.setMinutes(now.getMinutes() + otpExpiry);
601
- return {
602
- token: randomInt(100000, 999999).toString(),
603
- expiresAt: now,
604
- };
605
- }
606
-
607
- private getDummyOtpForUser(user?: User): string | undefined {
608
- const dummyOtp = this.settingService.getConfigValue<SolidCoreSetting>('dummyOtp');
609
- if (!dummyOtp || !user?.username) {
610
- return undefined;
611
- }
612
- const allowedUsers = this.getDummyOtpUsers();
613
- if (!allowedUsers.size) {
614
- return undefined;
615
- }
616
- const username = user.username.trim().toLowerCase();
617
- if (!username) {
618
- return undefined;
619
- }
620
- return allowedUsers.has(username) ? dummyOtp : undefined;
621
- }
622
-
623
- private getDummyOtpUsers(): Set<string> {
624
- const rawUsers = this.settingService.getConfigValue<SolidCoreSetting>('dummyOtpUsers');
625
- if (!rawUsers || typeof rawUsers !== 'string') {
626
- return new Set();
627
- }
628
- return new Set(
629
- rawUsers
630
- .split(',')
631
- .map((value) => value.trim().toLowerCase())
632
- .filter(Boolean),
182
+ }
183
+ const entity = await provider.buildExtensionEntity(signUpDto);
184
+ const effectiveDto = { ...signUpDto, roles: provider.roles(signUpDto) };
185
+ return this.performSignUp(
186
+ effectiveDto,
187
+ entity,
188
+ provider.repo as Repository<User>,
189
+ );
190
+ }
191
+ return this.performSignUp(signUpDto, new User(), this.userRepository);
192
+ }
193
+
194
+ private async performSignUp<T extends User>(
195
+ signUpDto: SignUpDto,
196
+ entity: T,
197
+ repo: Repository<T>,
198
+ ): Promise<T> {
199
+ try {
200
+ const onForcePasswordChange =
201
+ this.settingService.getConfigValue<SolidCoreSetting>(
202
+ "forceChangePasswordOnFirstLogin",
633
203
  );
634
- }
635
-
636
- async signIn(signInDto: SignInDto) {
637
- const user = await this.resolveUser(signInDto.username, signInDto.email);
638
- if (!user) {
639
- throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
640
- }
641
- await this.validateUserForPasswordLogin(user, signInDto.password);
642
- await this.rehashPasswordIfRequired(user, signInDto.password);
643
- await this.resetFailedAttempts(user);
644
-
645
- const tokens = await this.generateTokens(user);
646
-
647
- await this.userActivityHistoryService.logEvent('login', user);
648
-
649
- return {
650
- user: {
651
- email: user.email,
652
- mobile: user.mobile,
653
- username: user.username,
654
- forcePasswordChange: user.forcePasswordChange,
655
- id: user.id,
656
- roles: user.roles.map((role) => role.name)
657
- },
658
- ...tokens
659
- }
660
- }
661
-
662
- private maskEmail(email: string): string {
663
- if (!email) return null;
664
-
665
- const [localPart, domain] = email.split('@');
666
- if (localPart.length <= 2) {
667
- return `${localPart[0]}***@${domain}`;
668
- }
669
-
670
- const visibleStart = localPart.slice(0, 2);
671
- const visibleEnd = localPart.slice(-1);
672
- return `${visibleStart}***${visibleEnd}@${domain}`;
673
- }
674
-
675
- private maskMobile(mobile: string): string {
676
- if (!mobile) return null;
677
-
678
- if (mobile.length <= 4) {
679
- return mobile;
680
- }
681
-
682
- const visibleEnd = mobile.slice(-4);
683
- return `***${visibleEnd}`;
684
- }
685
-
686
- async otpInitiateLogin(signInDto: OTPSignInDto) {
687
- const isPasswordlessRegistrationEnabled = await this.isPasswordlessRegistrationEnabled();
688
- if (!isPasswordlessRegistrationEnabled) {
689
- throw new BadRequestException(ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED);
690
- }
691
-
692
- const type = this.resolveLoginType(signInDto);
693
- const user = await this.findUserForLogin(type, signInDto.identifier);
694
- const dummyOtp = this.getDummyOtpForUser(user);
695
- if (!dummyOtp) {
696
- await this.assignLoginOtp(user, type);
697
- this.notifyUserOnOtpInititateLogin(user, type);
698
- }
699
- return this.buildLoginOtpResponse(user, type);
700
- }
701
-
702
- private resolveLoginType(signInDto: OTPSignInDto): PasswordlessLoginValidateWhatSources {
703
- const setting = this.settingService.getConfigValue<SolidCoreSetting>('passwordlessLoginValidateWhat') as PasswordlessLoginValidateWhatSources;
704
-
705
- if (setting === PasswordlessLoginValidateWhatSources.SELECTABLE) {
706
- if (signInDto.type !== PasswordlessLoginValidateWhatSources.EMAIL &&
707
- signInDto.type !== PasswordlessLoginValidateWhatSources.MOBILE) {
708
- throw new BadRequestException(ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
709
- }
710
- return signInDto.type as PasswordlessLoginValidateWhatSources;
711
- }
712
-
713
- if (setting === PasswordlessLoginValidateWhatSources.EMAIL ||
714
- setting === PasswordlessLoginValidateWhatSources.MOBILE) {
715
- return setting;
716
- }
717
-
718
- throw new BadRequestException(ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
719
- }
720
-
721
- private async findUserForLogin(
722
- type: PasswordlessLoginValidateWhatSources,
723
- identifier: string,
724
- options: { withRoles?: boolean } = {},
725
- ): Promise<User> {
726
- const typeWhere = type === PasswordlessLoginValidateWhatSources.EMAIL
727
- ? { email: identifier }
728
- : { mobile: identifier };
729
- const user = await this.userRepository.findOne({
730
- where: [{ username: identifier }, typeWhere],
731
- ...(options.withRoles ? { relations: { roles: true } } : {}),
732
- });
733
- if (!user) {
734
- throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND);
735
- }
736
- if (!user.active) {
737
- throw new UnauthorizedException(ERROR_MESSAGES.USER_INACTIVE);
738
- }
739
- return user;
740
- }
741
-
742
- private async assignLoginOtp(user: User, type: PasswordlessLoginValidateWhatSources): Promise<void> {
743
- const { token, expiresAt } = await this.otp();
744
- if (type === PasswordlessLoginValidateWhatSources.EMAIL) {
745
- user.emailVerificationTokenOnLogin = token;
746
- user.emailVerificationTokenOnLoginExpiresAt = expiresAt;
747
- await this.userRepository.update(user.id, {
748
- emailVerificationTokenOnLogin: token,
749
- emailVerificationTokenOnLoginExpiresAt: expiresAt,
750
- });
751
- } else {
752
- user.mobileVerificationTokenOnLogin = token;
753
- user.mobileVerificationTokenOnLoginExpiresAt = expiresAt;
754
- await this.userRepository.update(user.id, {
755
- mobileVerificationTokenOnLogin: token,
756
- mobileVerificationTokenOnLoginExpiresAt: expiresAt,
757
- });
758
- }
759
- }
760
-
761
- private buildLoginOtpResponse(user: User, type: PasswordlessLoginValidateWhatSources) {
762
- const maskedIdentifier = type === PasswordlessLoginValidateWhatSources.EMAIL
763
- ? { email: this.maskEmail(user.email) }
764
- : { mobile: this.maskMobile(user.mobile) };
765
- return { message: SUCCESS_MESSAGES.OTP_SENT_SUCCESS_LOGIN, user: maskedIdentifier };
766
- }
767
-
768
- private async notifyUserOnOtpInititateLogin(user: User, loginType: PasswordlessLoginValidateWhatSources) {
769
- const companyLogo = await this.getCompanyLogo();
770
- const dummyOtp = this.getDummyOtpForUser(user);
771
-
772
- if (dummyOtp)
773
- return; // Do nothing if dummy otp is configured.
774
- if (loginType === PasswordlessLoginValidateWhatSources.EMAIL) {
775
- const mailService = this.mailServiceFactory.getMailService();
776
- mailService.sendEmailUsingTemplate(
777
- user.email,
778
- 'otp-on-login',
779
- {
780
- solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
781
- solidAppWebsiteUrl: this.settingService.getConfigValue<SolidCoreSetting>('solidAppWebsiteUrl'),
782
- firstName: user.username,
783
- emailVerificationTokenOnLogin: user.emailVerificationTokenOnLogin,
784
- fullName: user.fullName ? user.fullName : user.username,
785
- companyLogoUrl: companyLogo
786
- },
787
- this.settingService.getConfigValue<SolidCoreSetting>('shouldQueueEmails'),
788
- null,
789
- null,
790
- 'user',
791
- user.id
792
- );
793
- }
794
- if (loginType === PasswordlessLoginValidateWhatSources.MOBILE) {
795
- const smsService = this.smsFactory.getSmsService();
796
- smsService.sendSMSUsingTemplate(
797
- user.mobile,
798
- 'otp-on-login',
799
- {
800
- solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
801
- otp: user.mobileVerificationTokenOnLogin,
802
- mobileVerificationTokenOnLogin: user.mobileVerificationTokenOnLogin,
803
- firstName: user.username,
804
- fullName: user.fullName ? user.fullName : user.username,
805
- companyLogoUrl: companyLogo
806
- },
807
- this.settingService.getConfigValue<SolidCoreSetting>('shouldQueueSms'),
808
-
809
- );
810
- }
811
- }
812
-
813
- async otpConfirmLogin(confirmSignInDto: OTPConfirmOTPDto) {
814
- const isPasswordlessRegistrationEnabled = await this.isPasswordlessRegistrationEnabled();
815
- if (!isPasswordlessRegistrationEnabled) {
816
- throw new BadRequestException(ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED);
817
- }
818
-
819
- const { type, identifier, otp } = confirmSignInDto;
820
- if (type !== PasswordlessLoginValidateWhatSources.EMAIL &&
821
- type !== PasswordlessLoginValidateWhatSources.MOBILE) {
822
- throw new BadRequestException(ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
823
- }
824
-
825
- const user = await this.findUserForLogin(type, identifier, { withRoles: true });
826
- this.checkAccountBlocked(user);
827
- const dummyOtp = this.getDummyOtpForUser(user);
828
-
829
- if (dummyOtp) {
830
- if (otp !== dummyOtp) {
831
- throw new UnauthorizedException(ERROR_MESSAGES.INVALID_OTP);
832
- }
833
- return this.buildLoginTokenResponse(user);
834
- }
835
-
836
- try {
837
- this.validateLoginOtp(user, otp, type);
838
- } catch (e) {
839
- await this.incrementFailedAttempts(user);
840
- throw e;
841
- }
842
-
843
- await this.clearLoginOtp(user, type);
844
- await this.userActivityHistoryService.logEvent('login', user);
845
- await this.resetFailedAttempts(user);
846
- return this.buildLoginTokenResponse(user);
847
- }
848
-
849
- private validateLoginOtp(user: User, otp: string, type: PasswordlessLoginValidateWhatSources): void {
850
- const isEmail = type === PasswordlessLoginValidateWhatSources.EMAIL;
851
- const token = isEmail ? user.emailVerificationTokenOnLogin : user.mobileVerificationTokenOnLogin;
852
- const expiresAt = isEmail ? user.emailVerificationTokenOnLoginExpiresAt : user.mobileVerificationTokenOnLoginExpiresAt;
853
-
854
- if (token !== otp) {
855
- throw new UnauthorizedException(ERROR_MESSAGES.INVALID_OTP);
856
- }
857
- if (expiresAt < new Date()) {
858
- throw new UnauthorizedException(ERROR_MESSAGES.OTP_EXPIRED);
859
- }
860
- }
861
-
862
- private async clearLoginOtp(user: User, type: PasswordlessLoginValidateWhatSources): Promise<void> {
863
- if (type === PasswordlessLoginValidateWhatSources.EMAIL) {
864
- const verifiedAt = new Date();
865
- user.emailVerifiedOnLoginAt = verifiedAt;
866
- user.emailVerificationTokenOnLogin = null;
867
- user.emailVerificationTokenOnLoginExpiresAt = null;
868
- await this.userRepository.update(user.id, {
869
- emailVerifiedOnLoginAt: verifiedAt,
870
- emailVerificationTokenOnLogin: null,
871
- emailVerificationTokenOnLoginExpiresAt: null,
872
- });
873
- } else {
874
- const verifiedAt = new Date();
875
- user.mobileVerifiedOnLoginAt = verifiedAt;
876
- user.mobileVerificationTokenOnLogin = null;
877
- user.mobileVerificationTokenOnLoginExpiresAt = null;
878
- await this.userRepository.update(user.id, {
879
- mobileVerifiedOnLoginAt: verifiedAt,
880
- mobileVerificationTokenOnLogin: null,
881
- mobileVerificationTokenOnLoginExpiresAt: null,
882
- });
883
- }
884
- }
885
-
886
- private buildUserPayload(user: User) {
887
- const { id, username, email, mobile, lastLoginProvider } = user;
888
- const roles = user.roles.map((role) => role.name);
889
- return { id, username, email, mobile, lastLoginProvider, roles };
890
- }
891
-
892
- private async buildLoginTokenResponse(user: User) {
893
- const { accessToken, refreshToken } = await this.generateTokens(user);
894
- return { accessToken, refreshToken, user: this.buildUserPayload(user) };
895
- }
896
-
897
- async changePassword(changePasswordDto: ChangePasswordDto, activeUser: ActiveUserData) {
898
- const user = await this.userRepository.findOne({
899
- where: { id: changePasswordDto.id }
900
- });
901
- if (!user) {
902
- throw new NotFoundException(ERROR_MESSAGES.USER_NOT_FOUND);
903
- }
904
-
905
- if (!user.active) {
906
- throw new UnauthorizedException(ERROR_MESSAGES.USER_INACTIVE);
907
- }
908
-
909
- // 2. Validate if user has used a provider which is "local", only then it makes sense for us to initiate the forgot password routine.
910
- if (user.lastLoginProvider !== 'local') {
911
- throw new BadRequestException(ERROR_MESSAGES.NON_LOCAL_PROVIDER);
912
- }
913
-
914
- // Check if ID's match
915
- if (!(user.id === activeUser.sub)) {
916
- throw new BadRequestException(ERROR_MESSAGES.USER_ID_MISMATCH);
917
- }
918
-
919
- // Check if username's match
920
- if (!(user.username === activeUser.username)) {
921
- throw new BadRequestException(ERROR_MESSAGES.USERNAME_MISMATCH);
922
- }
923
-
924
- // Check if old password is matching.
925
- const isEqual = await this.hashingService.compare(
926
- changePasswordDto.currentPassword,
927
- user.password,
928
- user.passwordSchemeVersion
204
+ const activateUserOnRegistration =
205
+ this.settingService.getConfigValue<SolidCoreSetting>(
206
+ "activateUserOnRegistration",
929
207
  );
930
- if (!isEqual) {
931
- throw new UnauthorizedException(ERROR_MESSAGES.INCORRECT_CURRENT_PASSWORD);
932
- }
933
-
934
- // Update Password
935
- const pwdData = await this.userService.hashPassword(
936
- changePasswordDto.newPassword,
208
+ const defaultRole =
209
+ this.settingService.getConfigValue<SolidCoreSetting>("defaultRole");
210
+
211
+ var { user, pwd, autoGeneratedPwd } = await this.populateForSignup<T>(
212
+ entity,
213
+ signUpDto,
214
+ activateUserOnRegistration,
215
+ onForcePasswordChange,
216
+ );
217
+ const privateDto = signUpDto as { isAllowedToGenerateApiKeys?: boolean };
218
+ if (privateDto.isAllowedToGenerateApiKeys !== undefined) {
219
+ user.isAllowedToGenerateApiKeys = privateDto.isAllowedToGenerateApiKeys;
220
+ }
221
+ const savedUser = await repo.save(user);
222
+ const userRoles = signUpDto.roles ?? [];
223
+ if (
224
+ (signUpDto.roles?.length ?? 0) === 0 &&
225
+ signUpDto.username !== "sa" &&
226
+ defaultRole
227
+ ) {
228
+ userRoles.push(defaultRole);
229
+ }
230
+ await this.handlePostSignup(savedUser, userRoles, pwd, autoGeneratedPwd);
231
+
232
+ return savedUser;
233
+ } catch (err) {
234
+ const pgUniqueViolationErrorCode = "23505";
235
+ if (err.code === pgUniqueViolationErrorCode) {
236
+ throw new ConflictException(
237
+ parseUniqueConstraintError(
238
+ err.detail || ERROR_MESSAGES.UNIQUE_CONSTRAINT_VIOLATION,
239
+ ),
937
240
  );
938
- user.password = changePasswordDto.newPassword;
939
-
940
- user.password = pwdData.password;
941
- user.passwordScheme = pwdData.passwordScheme;
942
- user.passwordSchemeVersion = pwdData.passwordSchemeVersion;
943
- // Everytime the user changes the password we reset the forcePasswordChange flag back to false.
944
- user.forcePasswordChange = false;
945
-
946
- await this.userRepository.save(user);
947
-
948
- return true;
949
- }
950
-
951
- // generate uuid token for forgot password
952
- private async generateForgotPasswordToken(user?: User) {
953
- const expiryTime = new Date();
954
- const forgotPasswordVerificationTokenExpiry = this.settingService.getConfigValue<SolidCoreSetting>('forgotPasswordVerificationTokenExpiry');
955
- const dummyOtp = this.getDummyOtpForUser(user);
956
- expiryTime.setMinutes(expiryTime.getMinutes() + forgotPasswordVerificationTokenExpiry);
957
-
958
- return {
959
- token: dummyOtp ? dummyOtp : uuidv4(),
960
- expiresAt: expiryTime,
961
- };
962
- }
963
-
964
- async initiateForgotPassword(initiateForgotPasswordDto: InitiateForgotPasswordDto) {
965
- // Steps / Algorithm:
966
- // 1. Identify the user using the specified "username", if not found exit.
967
- // const user = await this.userRepository.findOne({
968
- // where: { username: initiateForgotPasswordDto.username, }
969
- // });
970
- const user = await this.resolveUser(initiateForgotPasswordDto.username, initiateForgotPasswordDto.email);
971
-
972
- let isValidUser = true // Instead of throwing exceptions we will simply return success message, this is to avoid user enumeration attacks.
973
- if (!user) {
974
- isValidUser = false
975
- // throw new NotFoundException(ERROR_MESSAGES.INVALID_CREDENTIALS);
976
- }
977
- if (isValidUser && !user?.active) {
978
- isValidUser = false
979
- // throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
980
- }
981
-
982
- // 2. Validate if user has used a provider which is "local", only then it makes sense for us to initiate the forgot password routine.
983
- if (isValidUser && user?.lastLoginProvider !== 'local') {
984
- isValidUser = false
985
- // throw new BadRequestException(ERROR_MESSAGES.INVALID_CREDENTIALS);
986
- }
987
-
988
- // 3. Generate a 6 digit validation token, we send this token to the user over their email & mobile number (controlled using configuration).
989
- // 4. Save this validation token in new fields on the user record.
990
- if (isValidUser) {
991
- const { token, expiresAt } = await this.generateForgotPasswordToken(user);
992
- user.verificationTokenOnForgotPassword = token;
993
- user.verificationTokenOnForgotPasswordExpiresAt = expiresAt;
994
- await this.userRepository.save(user);
995
- await this.notifyUserOnForgotPassword(user);
996
- }
997
-
998
- // 5. Return.
999
- return {
1000
- status: 'success',
1001
- message: SUCCESS_MESSAGES.FORGOT_PASSWORD_TOKEN_SENT,
1002
- error: '',
1003
- errorCode: '',
1004
- data: {
1005
- user: {
1006
- email: user?.email,
1007
- // mobile: user.mobile,
1008
- // username: user.username,
1009
- },
1010
- }
1011
- }
1012
- }
1013
-
1014
- private async notifyUserOnForgotPassword(user: User) {
1015
- const companyLogo = await this.getCompanyLogo();
1016
-
1017
- const forgotPasswordSendVerificationTokenOn = this.settingService.getConfigValue<SolidCoreSetting>('forgotPasswordSendVerificationTokenOn');
1018
-
1019
- if (forgotPasswordSendVerificationTokenOn == ForgotPasswordSendVerificationTokenOn.EMAIL) {
1020
- const mailService = this.mailServiceFactory.getMailService();
1021
- mailService.sendEmailUsingTemplate(
1022
- user.email,
1023
- 'forgot-password',
1024
- {
1025
- solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
1026
- solidAppWebsiteUrl: this.settingService.getConfigValue<SolidCoreSetting>('solidAppWebsiteUrl'),
1027
- firstName: user.username,
1028
- fullName: user.fullName,
1029
- // TODO: Need to prefix this with the page url where the forgot password page will open up.
1030
- passwordResetLink: `${this.settingService.getConfigValue<SolidCoreSetting>('frontendForgotPasswordPageUrl')}?token=${user.verificationTokenOnForgotPassword}`,
1031
- companyLogoUrl: companyLogo
1032
- },
1033
- this.settingService.getConfigValue<SolidCoreSetting>('shouldQueueEmails'),
1034
- null,
1035
- null,
1036
- 'user',
1037
- user.id
1038
- );
1039
- }
1040
- // Assuming all users do not have mobile as mandatory.
1041
- if (forgotPasswordSendVerificationTokenOn == ForgotPasswordSendVerificationTokenOn.MOBILE && user.mobile) {
1042
- const smsService = this.smsFactory.getSmsService();
1043
- smsService.sendSMSUsingTemplate(
1044
- user.mobile,
1045
- 'forgot-password',
1046
- {
1047
- solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
1048
- otp: user.verificationTokenOnForgotPassword,
1049
- verificationTokenOnForgotPassword: user.verificationTokenOnForgotPassword,
1050
- firstName: user.username,
1051
- companyLogoUrl: companyLogo
1052
- },
1053
- this.settingService.getConfigValue<SolidCoreSetting>('shouldQueueSms'),
1054
- );
1055
- }
1056
- }
1057
-
1058
- async confirmForgotPassword(confirmForgotPasswordDto: ConfirmForgotPasswordDto) {
1059
- return this.dataSource.transaction(async (m) => {
1060
- // Resolve the user id first (by username/email), but DON'T check the token in JS.
1061
- const user = await this.resolveUserByVerificationToken(confirmForgotPasswordDto.verificationToken);
1062
- if (!user) throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1063
- if (user.lastLoginProvider !== 'local') throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1064
- if (!user.active) throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1065
-
1066
- // 1) Atomically consume the token (only one request can succeed)
1067
- const { affected } = await m
1068
- .createQueryBuilder()
1069
- .update(User)
1070
- .set({
1071
- forgotPasswordConfirmedAt: () => 'NOW()',
1072
- verificationTokenOnForgotPassword: () => 'NULL',
1073
- verificationTokenOnForgotPasswordExpiresAt: () => 'NULL',
1074
- })
1075
- .where('id = :id', { id: user.id })
1076
- .andWhere('verificationTokenOnForgotPassword = :token', { token: confirmForgotPasswordDto.verificationToken })
1077
- .andWhere('verificationTokenOnForgotPasswordExpiresAt > NOW()')
1078
- .execute();
1079
-
1080
- if (affected !== 1) {
1081
- // Token invalid/expired/already used (or a parallel call already consumed it)
1082
- throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1083
- }
1084
-
1085
- // 2) Now update the password & history (still inside the same transaction)
1086
- const pwdHash = await this.hashingService.hash(confirmForgotPasswordDto.password);
1087
- const pwdScheme = this.hashingService.name(); // e.g. bcrypt
1088
- const pwdSchemeVersion = this.hashingService.currentVersion(); // e.g. 1, 2, 3 ...
1089
-
1090
- // Check reuse with your existing method (ensure it looks at hashes).
1091
- await m.getRepository(User).update({ id: user.id }, { password: pwdHash, passwordScheme: pwdScheme, passwordSchemeVersion: pwdSchemeVersion });
1092
- await this.notifyUserOnPasswordChanged(user);
1093
-
1094
- return {
1095
- status: 'success',
1096
- message: SUCCESS_MESSAGES.FORGOT_PASSWORD_CONFIRMED,
1097
- error: '',
1098
- errorCode: '',
1099
- data: {},
1100
- };
1101
- });
1102
- }
1103
-
1104
- private async notifyUserOnPasswordChanged(user: User) {
1105
- const companyLogo = await this.getCompanyLogo();
1106
- const forgotPasswordSendVerificationTokenOn = this.settingService.getConfigValue<SolidCoreSetting>('forgotPasswordSendVerificationTokenOn');
1107
-
1108
- if (forgotPasswordSendVerificationTokenOn == ForgotPasswordSendVerificationTokenOn.EMAIL) {
1109
- const mailService = this.mailServiceFactory.getMailService();
1110
- mailService.sendEmailUsingTemplate(
1111
- user.email,
1112
- 'password-changed',
1113
- {
1114
- solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
1115
- solidAppWebsiteUrl: this.settingService.getConfigValue<SolidCoreSetting>('solidAppWebsiteUrl'),
1116
- email: user.email,
1117
- firstName: user.username,
1118
- fullName: user.fullName,
1119
- // TODO: Need to prefix this with the page url where the forgot password page will open up.
1120
- passwordResetLink: `${this.settingService.getConfigValue<SolidCoreSetting>('frontendForgotPasswordPageUrl')}?token=${user.verificationTokenOnForgotPassword}`,
1121
- companyLogoUrl: companyLogo
1122
- },
1123
- this.settingService.getConfigValue<SolidCoreSetting>('shouldQueueEmails'),
1124
- null,
1125
- null,
1126
- 'user',
1127
- user.id
1128
- );
1129
- }
1130
- // Assuming all users do not have mobile as mandatory.
1131
- if (forgotPasswordSendVerificationTokenOn == ForgotPasswordSendVerificationTokenOn.MOBILE && user.mobile) {
1132
- const smsService = this.smsFactory.getSmsService();
1133
- smsService.sendSMSUsingTemplate(
1134
- user.mobile,
1135
- 'forgot-password',
1136
- {
1137
- solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
1138
- otp: user.verificationTokenOnForgotPassword,
1139
- verificationTokenOnForgotPassword: user.verificationTokenOnForgotPassword,
1140
- firstName: user.username,
1141
- companyLogoUrl: companyLogo
1142
- },
1143
- this.settingService.getConfigValue<SolidCoreSetting>('shouldQueueSms'),
1144
- );
1145
- }
241
+ }
242
+ throw err;
243
+ }
244
+ }
245
+
246
+ /** @deprecated Use IExtensionUserCreationProvider instead. Kept for backward compatibility. */
247
+ async signupForExtensionUser<T extends User>(
248
+ signUpDto: SignUpDto,
249
+ extensionUserDto: DeepPartial<T>,
250
+ extensionUserRepo: Repository<T>,
251
+ ): Promise<T> {
252
+ const entity = extensionUserRepo.create(extensionUserDto);
253
+ return this.performSignUp(signUpDto, entity, extensionUserRepo);
254
+ }
255
+
256
+ private async populateForSignup<T extends User>(
257
+ user: T,
258
+ signUpDto: SignUpDto,
259
+ isUserActive: boolean = true,
260
+ onForcePasswordChange?: boolean,
261
+ ) {
262
+ // const user = new User();
263
+ let autoGeneratedPwdPermission =
264
+ this.settingService.getConfigValue<SolidCoreSetting>(
265
+ "iamAutoGeneratedPassword",
266
+ );
267
+ if (signUpDto.roles && signUpDto.roles.length > 0) {
268
+ for (let i = 0; i < signUpDto.roles.length; i++) {
269
+ const roleName = signUpDto.roles[i];
270
+ await this.roleMetadataService.findRoleByName(roleName);
271
+ }
272
+ }
273
+ user.username = signUpDto.username;
274
+ user.email = signUpDto.email;
275
+ user.fullName = signUpDto.fullName;
276
+ user.forcePasswordChange = onForcePasswordChange;
277
+ if (signUpDto.mobile) {
278
+ user.mobile = signUpDto.mobile;
279
+ }
280
+ // this.logger.debug("user", user);
281
+
282
+ // If password has been specified by the user, then we simply create & activate the user based on the configuration parameter "activateUserOnRegistration".
283
+ let pwd = "";
284
+ let autoGeneratedPwd = "";
285
+
286
+ // User has specified password
287
+ if (signUpDto.password) {
288
+ pwd = await this.hashingService.hash(signUpDto.password);
289
+ }
290
+ // User has not specified password
291
+ else {
292
+ // When user does not specify password, and system is configured to auto generate passwords.
293
+ if (autoGeneratedPwdPermission?.toString().toLowerCase() === "true") {
294
+ autoGeneratedPwd = this.generatePassword();
295
+ pwd = await this.hashingService.hash(autoGeneratedPwd);
296
+ user.forcePasswordChange = true;
297
+ }
298
+ // When user does not specify password, and system is not configured to auto generate passwords.
299
+ else {
300
+ // This means that most likely the system is going to be using password-less login.
301
+ // If that is not the case then we can raise a bad request exception...
302
+ if (!(await this.isPasswordlessRegistrationEnabled())) {
303
+ this.logger.error(
304
+ "User being created without password, and password less login is also not enabled in the system. Is this intentional?",
305
+ );
306
+ throw new BadRequestException(
307
+ ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED,
308
+ );
309
+ }
310
+
311
+ // Save the hash of the blank password, anyways since passwordless login is enabled it does not matter.
312
+ pwd = await this.hashingService.hash(pwd);
313
+ }
314
+ }
315
+
316
+ user.password = pwd;
317
+ user.passwordScheme = this.hashingService.name(); // e.g. bcrypt
318
+ user.passwordSchemeVersion = this.hashingService.currentVersion(); // e.g. 1, 2, 3 ...
319
+ user.active = isUserActive;
320
+ return { user, pwd, autoGeneratedPwd };
321
+ }
322
+
323
+ private async handlePostSignup(
324
+ user: User,
325
+ roles: string[] = [],
326
+ pwd: string,
327
+ autoGeneratedPwd: string,
328
+ ) {
329
+ await this.userService.initializeRolesForNewUser(roles, user);
330
+
331
+ // if forcePasswordChange is true, then we trigger an email to the user to change the password, this needs to be done using a queue.
332
+ // Create a new method like notifyUserOnForcePasswordChange, create a new email template we can call it on-force-password-change this template to include the random password
333
+ if (user.forcePasswordChange && autoGeneratedPwd) {
334
+ await this.notifyUserOnForcePasswordChange(user, autoGeneratedPwd);
335
+ }
336
+
337
+ // Send welcome notifications (email/SMS) if enabled.
338
+ await this.notifyUserOnSignup(user);
339
+ }
340
+
341
+ generatePassword(length: number = 8): string {
342
+ const upperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
343
+ const lowerCase = "abcdefghijklmnopqrstuvwxyz";
344
+ const numbers = "0123456789";
345
+ const specialChars = "@$#";
346
+ const allChars = upperCase + lowerCase + numbers + specialChars;
347
+
348
+ let password = "";
349
+
350
+ for (let i = 0; i < length; i++) {
351
+ const randomIndex = Math.floor(Math.random() * allChars.length);
352
+ password += allChars[randomIndex];
353
+ }
354
+
355
+ return password;
356
+ }
357
+
358
+ private async notifyUserOnForcePasswordChange(
359
+ user: User,
360
+ autoGeneratedPwd: string,
361
+ ) {
362
+ const companyLogo = await this.getCompanyLogo();
363
+ const mailService = this.mailServiceFactory.getMailService();
364
+ mailService.sendEmailUsingTemplate(
365
+ user.email,
366
+ "on-force-password-change",
367
+ {
368
+ solidAppName:
369
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
370
+ solidAppWebsiteUrl:
371
+ this.settingService.getConfigValue<SolidCoreSetting>(
372
+ "solidAppWebsiteUrl",
373
+ ),
374
+ frontendLoginPageUrl:
375
+ this.settingService.getConfigValue<SolidCoreSetting>(
376
+ "frontendLoginPageUrl",
377
+ ),
378
+ email: user.email,
379
+ fullName: user.fullName,
380
+ userName: user.username,
381
+ password: autoGeneratedPwd,
382
+ companyLogoUrl: companyLogo,
383
+ },
384
+ this.settingService.getConfigValue<SolidCoreSetting>("shouldQueueEmails"),
385
+ null,
386
+ null,
387
+ "user",
388
+ user.id,
389
+ );
390
+ }
391
+
392
+ private async isWelcomeEmailEnabled(): Promise<boolean> {
393
+ const sendWelcomeEmailOnSignup =
394
+ this.settingService.getConfigValue<SolidCoreSetting>(
395
+ "sendWelcomeEmailOnSignup",
396
+ );
397
+ return sendWelcomeEmailOnSignup;
398
+ }
399
+
400
+ private async isWelcomeSmsEnabled(): Promise<boolean> {
401
+ const sendWelcomeSmsOnSignup =
402
+ this.settingService.getConfigValue<SolidCoreSetting>(
403
+ "sendWelcomeSmsOnSignup",
404
+ );
405
+ return sendWelcomeSmsOnSignup;
406
+ }
407
+
408
+ private async notifyUserOnSignup(user: User) {
409
+ const companyLogo = await this.getCompanyLogo();
410
+ // Email welcome
411
+ if (await this.isWelcomeEmailEnabled()) {
412
+ const mailService = this.mailServiceFactory.getMailService();
413
+ mailService.sendEmailUsingTemplate(
414
+ user.email,
415
+ "email-on-signup",
416
+ {
417
+ solidAppName:
418
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
419
+ solidAppWebsiteUrl:
420
+ this.settingService.getConfigValue<SolidCoreSetting>(
421
+ "solidAppWebsiteUrl",
422
+ ),
423
+ frontendLoginPageUrl:
424
+ this.settingService.getConfigValue<SolidCoreSetting>(
425
+ "frontendLoginPageUrl",
426
+ ),
427
+ email: user.email,
428
+ fullName: user.fullName,
429
+ userName: user.username,
430
+ companyLogoUrl: companyLogo,
431
+ },
432
+ this.settingService.getConfigValue<SolidCoreSetting>(
433
+ "shouldQueueEmails",
434
+ ),
435
+ null,
436
+ null,
437
+ "user",
438
+ user.id,
439
+ );
440
+ }
441
+
442
+ // SMS welcome
443
+ const isWelcomeSmsEnabled = await this.isWelcomeSmsEnabled();
444
+ if (isWelcomeSmsEnabled && user.mobile) {
445
+ const smsService = this.smsFactory.getSmsService();
446
+ smsService.sendSMSUsingTemplate(
447
+ user.mobile,
448
+ "text-on-signup",
449
+ {
450
+ solidAppName:
451
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
452
+ frontendLoginPageUrl:
453
+ this.settingService.getConfigValue<SolidCoreSetting>(
454
+ "frontendLoginPageUrl",
455
+ ),
456
+ firstName: user.username,
457
+ fullName: user.fullName ? user.fullName : user.username,
458
+ },
459
+ this.settingService.getConfigValue<SolidCoreSetting>("shouldQueueSms"),
460
+ );
461
+ }
462
+ }
463
+
464
+ async otpInitiateRegistration(signUpDto: OTPSignUpDto) {
465
+ const isPasswordlessRegistrationEnabled =
466
+ await this.isPasswordlessRegistrationEnabled();
467
+ if (!isPasswordlessRegistrationEnabled) {
468
+ throw new BadRequestException(
469
+ ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED,
470
+ );
471
+ }
472
+
473
+ const validationSource = this.resolvePasswordlessValidationSource();
474
+ this.validateOtpRegistrationInput(signUpDto, validationSource);
475
+
476
+ const existingUser = await this.findExistingRegistrationUser(signUpDto);
477
+ if (isNotEmpty(existingUser) && existingUser.active) {
478
+ throw new ConflictException(ERROR_MESSAGES.USER_ALREADY_EXISTS);
479
+ }
480
+
481
+ try {
482
+ const user = await this.upsertUserWithRegistrationVerificationTokens(
483
+ existingUser,
484
+ signUpDto,
485
+ validationSource,
486
+ );
487
+ await this.notifyUserOnOtpInitiateRegistration(user, validationSource);
488
+ } catch (err) {
489
+ if (err.code === "23505") {
490
+ throw new ConflictException(ERROR_MESSAGES.USER_ALREADY_EXISTS);
491
+ }
492
+ throw err;
493
+ }
494
+
495
+ return { message: SUCCESS_MESSAGES.OTP_SENT_SUCCESS_REGISTRATION };
496
+ }
497
+
498
+ private validateOtpRegistrationInput(
499
+ signUpDto: OTPSignUpDto,
500
+ validationSource: string,
501
+ ): void {
502
+ if (
503
+ validationSource === PasswordlessRegistrationValidateWhatSources.EMAIL &&
504
+ isEmpty(signUpDto.email)
505
+ ) {
506
+ throw new BadRequestException(
507
+ ERROR_MESSAGES.EMAIL_REQUIRED_FOR_VALIDATION,
508
+ );
1146
509
  }
1147
-
1148
- async generateTokens(user: User) {
1149
-
1150
- const [accessToken, refreshToken] = await Promise.all([
1151
- await this.generateAccessToken(user),
1152
- await this.generateRefreshToken(user),
1153
- ]);
1154
-
1155
- return {
1156
- accessToken,
1157
- refreshToken,
1158
- };
510
+ if (
511
+ validationSource === PasswordlessRegistrationValidateWhatSources.MOBILE &&
512
+ isEmpty(signUpDto.mobile)
513
+ ) {
514
+ throw new BadRequestException(
515
+ ERROR_MESSAGES.MOBILE_REQUIRED_FOR_VALIDATION,
516
+ );
517
+ }
518
+ }
519
+
520
+ private async findExistingRegistrationUser(
521
+ signUpDto: OTPSignUpDto,
522
+ ): Promise<User> {
523
+ return this.userRepository.findOne({
524
+ //TODO Perhaps we should use the user service instead of the repository directly.
525
+ where: [
526
+ { email: signUpDto.email },
527
+ { mobile: signUpDto.mobile },
528
+ { username: signUpDto.username },
529
+ ],
530
+ });
531
+ }
532
+
533
+ private resolvePasswordlessValidationSource(): string {
534
+ return this.settingService.getConfigValue<SolidCoreSetting>(
535
+ "passwordlessRegistrationValidateWhat",
536
+ );
537
+ }
538
+
539
+ private async upsertUserWithRegistrationVerificationTokens(
540
+ existingUser: User,
541
+ signUpDto: OTPSignUpDto,
542
+ validationSource: string,
543
+ ): Promise<User> {
544
+ let user = existingUser;
545
+ if (isEmpty(user)) {
546
+ user = this.createUser(signUpDto);
547
+ user.active = false; // User will be activated only after OTP verification, hence setting active to false for new user.
548
+ await this.assignRegistrationOtp(validationSource, user);
549
+ await this.userRepository.save(user);
550
+ await this.userService.addRoleToUser(
551
+ user.username,
552
+ this.settingService.getConfigValue<SolidCoreSetting>("defaultRole"),
553
+ );
554
+ } else {
555
+ await this.assignRegistrationOtp(validationSource, user);
556
+ await this.userRepository.save(user);
557
+ }
558
+ return user;
559
+ }
560
+
561
+ // Create a new user entity.
562
+ private createUser(signUpDto: OTPSignUpDto) {
563
+ const user = new User();
564
+ user.username = signUpDto.username;
565
+ user.email = signUpDto.email;
566
+ user.mobile = signUpDto.mobile;
567
+ user.customPayload = signUpDto.customPayload;
568
+ user.lastLoginProvider = LoginProvider.OTP;
569
+ return user;
570
+ }
571
+
572
+ // Generate the validation tokens for the user i.e (system configured + user provided)
573
+ private async assignRegistrationOtp(
574
+ passwordlessRegistrationValidateWhat: string,
575
+ user: User,
576
+ ) {
577
+ if (!passwordlessRegistrationValidateWhat) {
578
+ throw new BadRequestException(ERROR_MESSAGES.VALIDATION_SOURCE_REQUIRED);
579
+ }
580
+ const autoLoginUserOnRegistration =
581
+ this.settingService.getConfigValue<SolidCoreSetting>(
582
+ "autoLoginUserOnRegistration",
583
+ );
584
+ if (
585
+ passwordlessRegistrationValidateWhat ===
586
+ PasswordlessRegistrationValidateWhatSources.EMAIL
587
+ ) {
588
+ const { token, expiresAt } = await this.otp();
589
+ user.emailVerificationTokenOnRegistration = token;
590
+ user.emailVerificationTokenOnRegistrationExpiresAt = expiresAt;
591
+ if (autoLoginUserOnRegistration) {
592
+ user.emailVerificationTokenOnLogin = token;
593
+ user.emailVerificationTokenOnLoginExpiresAt = expiresAt;
594
+ }
595
+ }
596
+ if (
597
+ passwordlessRegistrationValidateWhat ===
598
+ PasswordlessRegistrationValidateWhatSources.MOBILE
599
+ ) {
600
+ const { token, expiresAt } = await this.otp();
601
+ user.mobileVerificationTokenOnRegistration = token;
602
+ user.mobileVerificationTokenOnRegistrationExpiresAt = expiresAt;
603
+ if (autoLoginUserOnRegistration) {
604
+ user.mobileVerificationTokenOnLogin = token;
605
+ user.mobileVerificationTokenOnLoginExpiresAt = expiresAt;
606
+ }
607
+ }
608
+ }
609
+
610
+ private async notifyUserOnOtpInitiateRegistration(
611
+ user: User,
612
+ registrationValidationSource: string,
613
+ ) {
614
+ const companyLogo = await this.getCompanyLogo();
615
+ if (
616
+ registrationValidationSource ===
617
+ PasswordlessLoginValidateWhatSources.EMAIL
618
+ ) {
619
+ const mailService = this.mailServiceFactory.getMailService();
620
+ mailService.sendEmailUsingTemplate(
621
+ user.email,
622
+ "otp-on-register",
623
+ {
624
+ solidAppName:
625
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
626
+ solidAppWebsiteUrl:
627
+ this.settingService.getConfigValue<SolidCoreSetting>(
628
+ "solidAppWebsiteUrl",
629
+ ),
630
+ firstName: user.username,
631
+ fullName: user.fullName ? user.fullName : user.username,
632
+ emailVerificationTokenOnRegistration:
633
+ user.emailVerificationTokenOnRegistration,
634
+ companyLogoUrl: companyLogo,
635
+ },
636
+ this.settingService.getConfigValue<SolidCoreSetting>(
637
+ "shouldQueueEmails",
638
+ ),
639
+ null,
640
+ null,
641
+ "user",
642
+ user.id,
643
+ );
644
+ }
645
+ if (
646
+ registrationValidationSource ===
647
+ PasswordlessLoginValidateWhatSources.MOBILE
648
+ ) {
649
+ const smsService = this.smsFactory.getSmsService();
650
+ smsService.sendSMSUsingTemplate(
651
+ user.mobile,
652
+ "otp-on-register",
653
+ {
654
+ solidAppName:
655
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
656
+ otp: user.mobileVerificationTokenOnRegistration,
657
+ mobileVerificationTokenOnRegistration:
658
+ user.mobileVerificationTokenOnRegistration,
659
+ firstName: user.username,
660
+ fullName: user.fullName ? user.fullName : user.username,
661
+ companyLogoUrl: companyLogo,
662
+ },
663
+ this.settingService.getConfigValue<SolidCoreSetting>("shouldQueueSms"),
664
+ );
665
+ }
666
+ }
667
+
668
+ async otpConfirmRegistration(confirmSignUpDto: OTPConfirmOTPDto) {
669
+ const isPasswordlessRegistrationEnabled =
670
+ await this.isPasswordlessRegistrationEnabled();
671
+ if (!isPasswordlessRegistrationEnabled) {
672
+ throw new BadRequestException(
673
+ ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED,
674
+ );
675
+ }
676
+
677
+ const { type, identifier, otp } = confirmSignUpDto;
678
+ if (
679
+ type !== PasswordlessRegistrationValidateWhatSources.EMAIL &&
680
+ type !== PasswordlessRegistrationValidateWhatSources.MOBILE
681
+ ) {
682
+ throw new BadRequestException(ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
683
+ }
684
+
685
+ const user = await this.findUserByRegistrationIdentifier(type, identifier);
686
+ this.validateRegistrationOtp(user, otp, type);
687
+ this.clearRegistrationOtp(user, type);
688
+ user.active =
689
+ this.settingService.getConfigValue<SolidCoreSetting>(
690
+ "activateUserOnRegistration",
691
+ ) &&
692
+ (await this.areAllPasswordlessRegistrationValidationSourcesVerified(
693
+ user,
694
+ ));
695
+
696
+ const savedUser: User = await this.userRepository.save(user);
697
+ this.triggerRegistrationEvent(savedUser);
698
+ return {
699
+ active: savedUser.active,
700
+ message: `User registration verified for ${type}`,
701
+ };
702
+ }
703
+
704
+ private async findUserByRegistrationIdentifier(
705
+ type: PasswordlessRegistrationValidateWhatSources,
706
+ identifier: string,
707
+ ): Promise<User> {
708
+ const where =
709
+ type === PasswordlessRegistrationValidateWhatSources.EMAIL
710
+ ? { email: identifier }
711
+ : { mobile: identifier };
712
+ const user = await this.userRepository.findOne({ where });
713
+ if (!user) {
714
+ throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND);
715
+ }
716
+ return user;
717
+ }
718
+
719
+ private validateRegistrationOtp(
720
+ user: User,
721
+ otp: string,
722
+ type: PasswordlessRegistrationValidateWhatSources,
723
+ ): void {
724
+ const isEmail = type === PasswordlessRegistrationValidateWhatSources.EMAIL;
725
+ const token = isEmail
726
+ ? user.emailVerificationTokenOnRegistration
727
+ : user.mobileVerificationTokenOnRegistration;
728
+ const expiresAt = isEmail
729
+ ? user.emailVerificationTokenOnRegistrationExpiresAt
730
+ : user.mobileVerificationTokenOnRegistrationExpiresAt;
731
+
732
+ if (token !== otp) {
733
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_OTP);
734
+ }
735
+ if (expiresAt < new Date()) {
736
+ throw new UnauthorizedException(ERROR_MESSAGES.OTP_EXPIRED);
737
+ }
738
+ }
739
+
740
+ private clearRegistrationOtp(
741
+ user: User,
742
+ type: PasswordlessRegistrationValidateWhatSources,
743
+ ): void {
744
+ if (type === PasswordlessRegistrationValidateWhatSources.EMAIL) {
745
+ user.emailVerifiedOnRegistrationAt = new Date();
746
+ user.emailVerificationTokenOnRegistration = null;
747
+ user.emailVerificationTokenOnRegistrationExpiresAt = null;
748
+ } else {
749
+ user.mobileVerifiedOnRegistrationAt = new Date();
750
+ user.mobileVerificationTokenOnRegistration = null;
751
+ user.mobileVerificationTokenOnRegistrationExpiresAt = null;
752
+ }
753
+ }
754
+
755
+ private triggerRegistrationEvent(savedUser: User) {
756
+ // Trigger events for user registration.
757
+ const event = new EventDetails<User>(EventType.USER_REGISTERED, savedUser);
758
+ this.eventEmitter.emit(EventType.USER_REGISTERED, event);
759
+ }
760
+
761
+ private async areAllPasswordlessRegistrationValidationSourcesVerified(
762
+ user: User,
763
+ ): Promise<boolean> {
764
+ const registrationValidationSource =
765
+ this.resolvePasswordlessValidationSource();
766
+ if (
767
+ registrationValidationSource ===
768
+ PasswordlessLoginValidateWhatSources.EMAIL
769
+ ) {
770
+ if (!user.emailVerifiedOnRegistrationAt) {
771
+ return false;
772
+ }
1159
773
  }
1160
-
1161
- async generateAccessToken(user: User) {
1162
-
1163
- // const userRoleNames = user.roles.map((role) => role.name).join(';')
1164
- const userRoleNames = user.roles.map((role) => role.name);
1165
-
1166
- const accessTokenTtl = this.settingService.getConfigValue<SolidCoreSetting>("accessTokenTtl");
1167
- const accessToken = await this.signToken<Partial<ActiveUserData>>(
1168
- user.id,
1169
- accessTokenTtl,
1170
- { username: user.username, email: user.email, roles: userRoleNames },
1171
- );
1172
-
1173
- return accessToken;
774
+ if (
775
+ registrationValidationSource ===
776
+ PasswordlessLoginValidateWhatSources.MOBILE
777
+ ) {
778
+ if (!user.mobileVerifiedOnRegistrationAt) {
779
+ return false;
780
+ }
781
+ }
782
+ return true;
783
+ }
784
+
785
+ private async otp(): Promise<otp> {
786
+ const now = new Date();
787
+ const otpExpiry =
788
+ this.settingService.getConfigValue<SolidCoreSetting>("otpExpiry");
789
+ now.setMinutes(now.getMinutes() + otpExpiry);
790
+ return {
791
+ token: randomInt(100000, 999999).toString(),
792
+ expiresAt: now,
793
+ };
794
+ }
795
+
796
+ private getDummyOtpForUser(user?: User): string | undefined {
797
+ const dummyOtp =
798
+ this.settingService.getConfigValue<SolidCoreSetting>("dummyOtp");
799
+ if (!dummyOtp || !user?.username) {
800
+ return undefined;
801
+ }
802
+ const allowedUsers = this.getDummyOtpUsers();
803
+ if (!allowedUsers.size) {
804
+ return undefined;
805
+ }
806
+ const username = user.username.trim().toLowerCase();
807
+ if (!username) {
808
+ return undefined;
809
+ }
810
+ return allowedUsers.has(username) ? dummyOtp : undefined;
811
+ }
812
+
813
+ private getDummyOtpUsers(): Set<string> {
814
+ const rawUsers =
815
+ this.settingService.getConfigValue<SolidCoreSetting>("dummyOtpUsers");
816
+ if (!rawUsers || typeof rawUsers !== "string") {
817
+ return new Set();
818
+ }
819
+ return new Set(
820
+ rawUsers
821
+ .split(",")
822
+ .map((value) => value.trim().toLowerCase())
823
+ .filter(Boolean),
824
+ );
825
+ }
826
+
827
+ async signIn(signInDto: SignInDto) {
828
+ const user = await this.resolveUser(signInDto.username, signInDto.email);
829
+ if (!user) {
830
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
831
+ }
832
+ await this.validateUserForPasswordLogin(user, signInDto.password);
833
+ await this.rehashPasswordIfRequired(user, signInDto.password);
834
+ await this.resetFailedAttempts(user);
835
+
836
+ const tokens = await this.generateTokens(user);
837
+
838
+ await this.userActivityHistoryService.logEvent("login", user);
839
+
840
+ return {
841
+ user: {
842
+ email: user.email,
843
+ mobile: user.mobile,
844
+ username: user.username,
845
+ forcePasswordChange: user.forcePasswordChange,
846
+ id: user.id,
847
+ roles: user.roles.map((role) => role.name),
848
+ },
849
+ ...tokens,
850
+ };
851
+ }
852
+
853
+ private maskEmail(email: string): string {
854
+ if (!email) return null;
855
+
856
+ const [localPart, domain] = email.split("@");
857
+ if (localPart.length <= 2) {
858
+ return `${localPart[0]}***@${domain}`;
859
+ }
860
+
861
+ const visibleStart = localPart.slice(0, 2);
862
+ const visibleEnd = localPart.slice(-1);
863
+ return `${visibleStart}***${visibleEnd}@${domain}`;
864
+ }
865
+
866
+ private maskMobile(mobile: string): string {
867
+ if (!mobile) return null;
868
+
869
+ if (mobile.length <= 4) {
870
+ return mobile;
871
+ }
872
+
873
+ const visibleEnd = mobile.slice(-4);
874
+ return `***${visibleEnd}`;
875
+ }
876
+
877
+ async otpInitiateLogin(signInDto: OTPSignInDto) {
878
+ const isPasswordlessRegistrationEnabled =
879
+ await this.isPasswordlessRegistrationEnabled();
880
+ if (!isPasswordlessRegistrationEnabled) {
881
+ throw new BadRequestException(
882
+ ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED,
883
+ );
884
+ }
885
+
886
+ const type = this.resolveLoginType(signInDto);
887
+ const user = await this.findUserForLogin(type, signInDto.identifier);
888
+ const dummyOtp = this.getDummyOtpForUser(user);
889
+ if (!dummyOtp) {
890
+ await this.assignLoginOtp(user, type);
891
+ this.notifyUserOnOtpInititateLogin(user, type);
892
+ }
893
+ return this.buildLoginOtpResponse(user, type);
894
+ }
895
+
896
+ private resolveLoginType(
897
+ signInDto: OTPSignInDto,
898
+ ): PasswordlessLoginValidateWhatSources {
899
+ const setting = this.settingService.getConfigValue<SolidCoreSetting>(
900
+ "passwordlessLoginValidateWhat",
901
+ ) as PasswordlessLoginValidateWhatSources;
902
+
903
+ if (setting === PasswordlessLoginValidateWhatSources.SELECTABLE) {
904
+ if (
905
+ signInDto.type !== PasswordlessLoginValidateWhatSources.EMAIL &&
906
+ signInDto.type !== PasswordlessLoginValidateWhatSources.MOBILE
907
+ ) {
908
+ throw new BadRequestException(ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
909
+ }
910
+ return signInDto.type as PasswordlessLoginValidateWhatSources;
1174
911
  }
1175
912
 
1176
- async generateRefreshToken(user: User, previousRefreshToken?: string) {
1177
- const refreshTokenId = randomUUID();
1178
- const refreshTokenTtl = this.settingService.getConfigValue<SolidCoreSetting>("refreshTokenTtl");
1179
- const refreshToken = await this.signToken(user.id, refreshTokenTtl, {
1180
- refreshTokenId,
913
+ if (
914
+ setting === PasswordlessLoginValidateWhatSources.EMAIL ||
915
+ setting === PasswordlessLoginValidateWhatSources.MOBILE
916
+ ) {
917
+ return setting;
918
+ }
919
+
920
+ throw new BadRequestException(ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
921
+ }
922
+
923
+ private async findUserForLogin(
924
+ type: PasswordlessLoginValidateWhatSources,
925
+ identifier: string,
926
+ options: { withRoles?: boolean } = {},
927
+ ): Promise<User> {
928
+ const typeWhere =
929
+ type === PasswordlessLoginValidateWhatSources.EMAIL
930
+ ? { email: identifier }
931
+ : { mobile: identifier };
932
+ const user = await this.userRepository.findOne({
933
+ where: [{ username: identifier }, typeWhere],
934
+ ...(options.withRoles ? { relations: { roles: true } } : {}),
935
+ });
936
+ if (!user) {
937
+ throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND);
938
+ }
939
+ if (!user.active) {
940
+ throw new UnauthorizedException(ERROR_MESSAGES.USER_INACTIVE);
941
+ }
942
+ return user;
943
+ }
944
+
945
+ private async assignLoginOtp(
946
+ user: User,
947
+ type: PasswordlessLoginValidateWhatSources,
948
+ ): Promise<void> {
949
+ const { token, expiresAt } = await this.otp();
950
+ if (type === PasswordlessLoginValidateWhatSources.EMAIL) {
951
+ user.emailVerificationTokenOnLogin = token;
952
+ user.emailVerificationTokenOnLoginExpiresAt = expiresAt;
953
+ await this.userRepository.update(user.id, {
954
+ emailVerificationTokenOnLogin: token,
955
+ emailVerificationTokenOnLoginExpiresAt: expiresAt,
956
+ });
957
+ } else {
958
+ user.mobileVerificationTokenOnLogin = token;
959
+ user.mobileVerificationTokenOnLoginExpiresAt = expiresAt;
960
+ await this.userRepository.update(user.id, {
961
+ mobileVerificationTokenOnLogin: token,
962
+ mobileVerificationTokenOnLoginExpiresAt: expiresAt,
963
+ });
964
+ }
965
+ }
966
+
967
+ private buildLoginOtpResponse(
968
+ user: User,
969
+ type: PasswordlessLoginValidateWhatSources,
970
+ ) {
971
+ const maskedIdentifier =
972
+ type === PasswordlessLoginValidateWhatSources.EMAIL
973
+ ? { email: this.maskEmail(user.email) }
974
+ : { mobile: this.maskMobile(user.mobile) };
975
+ return {
976
+ message: SUCCESS_MESSAGES.OTP_SENT_SUCCESS_LOGIN,
977
+ user: maskedIdentifier,
978
+ };
979
+ }
980
+
981
+ private async notifyUserOnOtpInititateLogin(
982
+ user: User,
983
+ loginType: PasswordlessLoginValidateWhatSources,
984
+ ) {
985
+ const companyLogo = await this.getCompanyLogo();
986
+ const dummyOtp = this.getDummyOtpForUser(user);
987
+
988
+ if (dummyOtp) return; // Do nothing if dummy otp is configured.
989
+ if (loginType === PasswordlessLoginValidateWhatSources.EMAIL) {
990
+ const mailService = this.mailServiceFactory.getMailService();
991
+ mailService.sendEmailUsingTemplate(
992
+ user.email,
993
+ "otp-on-login",
994
+ {
995
+ solidAppName:
996
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
997
+ solidAppWebsiteUrl:
998
+ this.settingService.getConfigValue<SolidCoreSetting>(
999
+ "solidAppWebsiteUrl",
1000
+ ),
1001
+ firstName: user.username,
1002
+ emailVerificationTokenOnLogin: user.emailVerificationTokenOnLogin,
1003
+ fullName: user.fullName ? user.fullName : user.username,
1004
+ companyLogoUrl: companyLogo,
1005
+ },
1006
+ this.settingService.getConfigValue<SolidCoreSetting>(
1007
+ "shouldQueueEmails",
1008
+ ),
1009
+ null,
1010
+ null,
1011
+ "user",
1012
+ user.id,
1013
+ );
1014
+ }
1015
+ if (loginType === PasswordlessLoginValidateWhatSources.MOBILE) {
1016
+ const smsService = this.smsFactory.getSmsService();
1017
+ smsService.sendSMSUsingTemplate(
1018
+ user.mobile,
1019
+ "otp-on-login",
1020
+ {
1021
+ solidAppName:
1022
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
1023
+ otp: user.mobileVerificationTokenOnLogin,
1024
+ mobileVerificationTokenOnLogin: user.mobileVerificationTokenOnLogin,
1025
+ firstName: user.username,
1026
+ fullName: user.fullName ? user.fullName : user.username,
1027
+ companyLogoUrl: companyLogo,
1028
+ },
1029
+ this.settingService.getConfigValue<SolidCoreSetting>("shouldQueueSms"),
1030
+ );
1031
+ }
1032
+ }
1033
+
1034
+ async otpConfirmLogin(confirmSignInDto: OTPConfirmOTPDto) {
1035
+ const isPasswordlessRegistrationEnabled =
1036
+ await this.isPasswordlessRegistrationEnabled();
1037
+ if (!isPasswordlessRegistrationEnabled) {
1038
+ throw new BadRequestException(
1039
+ ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED,
1040
+ );
1041
+ }
1042
+
1043
+ const { type, identifier, otp } = confirmSignInDto;
1044
+ if (
1045
+ type !== PasswordlessLoginValidateWhatSources.EMAIL &&
1046
+ type !== PasswordlessLoginValidateWhatSources.MOBILE
1047
+ ) {
1048
+ throw new BadRequestException(ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
1049
+ }
1050
+
1051
+ const user = await this.findUserForLogin(type, identifier, {
1052
+ withRoles: true,
1053
+ });
1054
+ this.checkAccountBlocked(user);
1055
+ const dummyOtp = this.getDummyOtpForUser(user);
1056
+
1057
+ if (dummyOtp) {
1058
+ if (otp !== dummyOtp) {
1059
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_OTP);
1060
+ }
1061
+ return this.buildLoginTokenResponse(user);
1062
+ }
1063
+
1064
+ try {
1065
+ this.validateLoginOtp(user, otp, type);
1066
+ } catch (e) {
1067
+ await this.incrementFailedAttempts(user);
1068
+ throw e;
1069
+ }
1070
+
1071
+ await this.clearLoginOtp(user, type);
1072
+ await this.userActivityHistoryService.logEvent("login", user);
1073
+ await this.resetFailedAttempts(user);
1074
+ return this.buildLoginTokenResponse(user);
1075
+ }
1076
+
1077
+ private validateLoginOtp(
1078
+ user: User,
1079
+ otp: string,
1080
+ type: PasswordlessLoginValidateWhatSources,
1081
+ ): void {
1082
+ const isEmail = type === PasswordlessLoginValidateWhatSources.EMAIL;
1083
+ const token = isEmail
1084
+ ? user.emailVerificationTokenOnLogin
1085
+ : user.mobileVerificationTokenOnLogin;
1086
+ const expiresAt = isEmail
1087
+ ? user.emailVerificationTokenOnLoginExpiresAt
1088
+ : user.mobileVerificationTokenOnLoginExpiresAt;
1089
+
1090
+ if (token !== otp) {
1091
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_OTP);
1092
+ }
1093
+ if (expiresAt < new Date()) {
1094
+ throw new UnauthorizedException(ERROR_MESSAGES.OTP_EXPIRED);
1095
+ }
1096
+ }
1097
+
1098
+ private async clearLoginOtp(
1099
+ user: User,
1100
+ type: PasswordlessLoginValidateWhatSources,
1101
+ ): Promise<void> {
1102
+ if (type === PasswordlessLoginValidateWhatSources.EMAIL) {
1103
+ const verifiedAt = new Date();
1104
+ user.emailVerifiedOnLoginAt = verifiedAt;
1105
+ user.emailVerificationTokenOnLogin = null;
1106
+ user.emailVerificationTokenOnLoginExpiresAt = null;
1107
+ await this.userRepository.update(user.id, {
1108
+ emailVerifiedOnLoginAt: verifiedAt,
1109
+ emailVerificationTokenOnLogin: null,
1110
+ emailVerificationTokenOnLoginExpiresAt: null,
1111
+ });
1112
+ } else {
1113
+ const verifiedAt = new Date();
1114
+ user.mobileVerifiedOnLoginAt = verifiedAt;
1115
+ user.mobileVerificationTokenOnLogin = null;
1116
+ user.mobileVerificationTokenOnLoginExpiresAt = null;
1117
+ await this.userRepository.update(user.id, {
1118
+ mobileVerifiedOnLoginAt: verifiedAt,
1119
+ mobileVerificationTokenOnLogin: null,
1120
+ mobileVerificationTokenOnLoginExpiresAt: null,
1121
+ });
1122
+ }
1123
+ }
1124
+
1125
+ private buildUserPayload(user: User) {
1126
+ const { id, username, email, mobile, lastLoginProvider } = user;
1127
+ const roles = user.roles.map((role) => role.name);
1128
+ return { id, username, email, mobile, lastLoginProvider, roles };
1129
+ }
1130
+
1131
+ private async buildLoginTokenResponse(user: User) {
1132
+ const { accessToken, refreshToken } = await this.generateTokens(user);
1133
+ return { accessToken, refreshToken, user: this.buildUserPayload(user) };
1134
+ }
1135
+
1136
+ async changePassword(
1137
+ changePasswordDto: ChangePasswordDto,
1138
+ activeUser: ActiveUserData,
1139
+ ) {
1140
+ const user = await this.userRepository.findOne({
1141
+ where: { id: changePasswordDto.id },
1142
+ });
1143
+ if (!user) {
1144
+ throw new NotFoundException(ERROR_MESSAGES.USER_NOT_FOUND);
1145
+ }
1146
+
1147
+ if (!user.active) {
1148
+ throw new UnauthorizedException(ERROR_MESSAGES.USER_INACTIVE);
1149
+ }
1150
+
1151
+ // 2. Validate if user has used a provider which is "local", only then it makes sense for us to initiate the forgot password routine.
1152
+ if (user.lastLoginProvider !== "local") {
1153
+ throw new BadRequestException(ERROR_MESSAGES.NON_LOCAL_PROVIDER);
1154
+ }
1155
+
1156
+ // Check if ID's match
1157
+ if (!(user.id === activeUser.sub)) {
1158
+ throw new BadRequestException(ERROR_MESSAGES.USER_ID_MISMATCH);
1159
+ }
1160
+
1161
+ // Check if username's match
1162
+ if (!(user.username === activeUser.username)) {
1163
+ throw new BadRequestException(ERROR_MESSAGES.USERNAME_MISMATCH);
1164
+ }
1165
+
1166
+ // Check if old password is matching.
1167
+ const isEqual = await this.hashingService.compare(
1168
+ changePasswordDto.currentPassword,
1169
+ user.password,
1170
+ user.passwordSchemeVersion,
1171
+ );
1172
+ if (!isEqual) {
1173
+ throw new UnauthorizedException(
1174
+ ERROR_MESSAGES.INCORRECT_CURRENT_PASSWORD,
1175
+ );
1176
+ }
1177
+
1178
+ // Update Password
1179
+ const pwdData = await this.userService.hashPassword(
1180
+ changePasswordDto.newPassword,
1181
+ );
1182
+ user.password = changePasswordDto.newPassword;
1183
+
1184
+ user.password = pwdData.password;
1185
+ user.passwordScheme = pwdData.passwordScheme;
1186
+ user.passwordSchemeVersion = pwdData.passwordSchemeVersion;
1187
+ // Everytime the user changes the password we reset the forcePasswordChange flag back to false.
1188
+ user.forcePasswordChange = false;
1189
+
1190
+ await this.userRepository.save(user);
1191
+
1192
+ return true;
1193
+ }
1194
+
1195
+ // generate uuid token for forgot password
1196
+ private async generateForgotPasswordToken(user?: User) {
1197
+ const expiryTime = new Date();
1198
+ const forgotPasswordVerificationTokenExpiry =
1199
+ this.settingService.getConfigValue<SolidCoreSetting>(
1200
+ "forgotPasswordVerificationTokenExpiry",
1201
+ );
1202
+ const dummyOtp = this.getDummyOtpForUser(user);
1203
+ expiryTime.setMinutes(
1204
+ expiryTime.getMinutes() + forgotPasswordVerificationTokenExpiry,
1205
+ );
1206
+
1207
+ return {
1208
+ token: dummyOtp ? dummyOtp : uuidv4(),
1209
+ expiresAt: expiryTime,
1210
+ };
1211
+ }
1212
+
1213
+ async initiateForgotPassword(
1214
+ initiateForgotPasswordDto: InitiateForgotPasswordDto,
1215
+ ) {
1216
+ // Steps / Algorithm:
1217
+ // 1. Identify the user using the specified "username", if not found exit.
1218
+ // const user = await this.userRepository.findOne({
1219
+ // where: { username: initiateForgotPasswordDto.username, }
1220
+ // });
1221
+ const user = await this.resolveUser(
1222
+ initiateForgotPasswordDto.username,
1223
+ initiateForgotPasswordDto.email,
1224
+ );
1225
+
1226
+ let isValidUser = true; // Instead of throwing exceptions we will simply return success message, this is to avoid user enumeration attacks.
1227
+ if (!user) {
1228
+ isValidUser = false;
1229
+ // throw new NotFoundException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1230
+ }
1231
+ if (isValidUser && !user?.active) {
1232
+ isValidUser = false;
1233
+ // throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1234
+ }
1235
+
1236
+ // 2. Validate if user has used a provider which is "local", only then it makes sense for us to initiate the forgot password routine.
1237
+ if (isValidUser && user?.lastLoginProvider !== "local") {
1238
+ isValidUser = false;
1239
+ // throw new BadRequestException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1240
+ }
1241
+
1242
+ // 3. Generate a 6 digit validation token, we send this token to the user over their email & mobile number (controlled using configuration).
1243
+ // 4. Save this validation token in new fields on the user record.
1244
+ if (isValidUser) {
1245
+ const { token, expiresAt } = await this.generateForgotPasswordToken(user);
1246
+ user.verificationTokenOnForgotPassword = token;
1247
+ user.verificationTokenOnForgotPasswordExpiresAt = expiresAt;
1248
+ await this.userRepository.save(user);
1249
+ await this.notifyUserOnForgotPassword(user);
1250
+ }
1251
+
1252
+ // 5. Return.
1253
+ return {
1254
+ status: "success",
1255
+ message: SUCCESS_MESSAGES.FORGOT_PASSWORD_TOKEN_SENT,
1256
+ error: "",
1257
+ errorCode: "",
1258
+ data: {
1259
+ user: {
1260
+ email: user?.email,
1261
+ // mobile: user.mobile,
1262
+ // username: user.username,
1263
+ },
1264
+ },
1265
+ };
1266
+ }
1267
+
1268
+ private async notifyUserOnForgotPassword(user: User) {
1269
+ const companyLogo = await this.getCompanyLogo();
1270
+
1271
+ const forgotPasswordSendVerificationTokenOn =
1272
+ this.settingService.getConfigValue<SolidCoreSetting>(
1273
+ "forgotPasswordSendVerificationTokenOn",
1274
+ );
1275
+
1276
+ if (
1277
+ forgotPasswordSendVerificationTokenOn ==
1278
+ ForgotPasswordSendVerificationTokenOn.EMAIL
1279
+ ) {
1280
+ const mailService = this.mailServiceFactory.getMailService();
1281
+ mailService.sendEmailUsingTemplate(
1282
+ user.email,
1283
+ "forgot-password",
1284
+ {
1285
+ solidAppName:
1286
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
1287
+ solidAppWebsiteUrl:
1288
+ this.settingService.getConfigValue<SolidCoreSetting>(
1289
+ "solidAppWebsiteUrl",
1290
+ ),
1291
+ firstName: user.username,
1292
+ fullName: user.fullName,
1293
+ // TODO: Need to prefix this with the page url where the forgot password page will open up.
1294
+ passwordResetLink: `${this.settingService.getConfigValue<SolidCoreSetting>("frontendForgotPasswordPageUrl")}?token=${user.verificationTokenOnForgotPassword}`,
1295
+ companyLogoUrl: companyLogo,
1296
+ },
1297
+ this.settingService.getConfigValue<SolidCoreSetting>(
1298
+ "shouldQueueEmails",
1299
+ ),
1300
+ null,
1301
+ null,
1302
+ "user",
1303
+ user.id,
1304
+ );
1305
+ }
1306
+ // Assuming all users do not have mobile as mandatory.
1307
+ if (
1308
+ forgotPasswordSendVerificationTokenOn ==
1309
+ ForgotPasswordSendVerificationTokenOn.MOBILE &&
1310
+ user.mobile
1311
+ ) {
1312
+ const smsService = this.smsFactory.getSmsService();
1313
+ smsService.sendSMSUsingTemplate(
1314
+ user.mobile,
1315
+ "forgot-password",
1316
+ {
1317
+ solidAppName:
1318
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
1319
+ otp: user.verificationTokenOnForgotPassword,
1320
+ verificationTokenOnForgotPassword:
1321
+ user.verificationTokenOnForgotPassword,
1322
+ firstName: user.username,
1323
+ companyLogoUrl: companyLogo,
1324
+ },
1325
+ this.settingService.getConfigValue<SolidCoreSetting>("shouldQueueSms"),
1326
+ );
1327
+ }
1328
+ }
1329
+
1330
+ async confirmForgotPassword(
1331
+ confirmForgotPasswordDto: ConfirmForgotPasswordDto,
1332
+ ) {
1333
+ return this.dataSource.transaction(async (m) => {
1334
+ // Resolve the user id first (by username/email), but DON'T check the token in JS.
1335
+ const user = await this.resolveUserByVerificationToken(
1336
+ confirmForgotPasswordDto.verificationToken,
1337
+ );
1338
+ if (!user)
1339
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1340
+ if (user.lastLoginProvider !== "local")
1341
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1342
+ if (!user.active)
1343
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1344
+
1345
+ // 1) Atomically consume the token (only one request can succeed)
1346
+ const { affected } = await m
1347
+ .createQueryBuilder()
1348
+ .update(User)
1349
+ .set({
1350
+ forgotPasswordConfirmedAt: () => "NOW()",
1351
+ verificationTokenOnForgotPassword: () => "NULL",
1352
+ verificationTokenOnForgotPasswordExpiresAt: () => "NULL",
1181
1353
  })
1182
-
1183
- // store the refresh token id in the redis storage.
1184
- await this.refreshTokenIdsStorage.insert(user.id, refreshToken, previousRefreshToken);
1185
-
1186
- return refreshToken;
1187
- }
1188
-
1189
- async refreshTokens(refreshTokenDto: RefreshTokenDto) {
1190
- try {
1191
- const secret = this.settingService.getConfigValue<SolidCoreSetting>("secret");
1192
- const audience = this.settingService.getConfigValue<SolidCoreSetting>("audience");
1193
- const issuer = this.settingService.getConfigValue<SolidCoreSetting>("issuer");
1194
-
1195
- const { sub } = await this.jwtService.verifyAsync<Pick<ActiveUserData, 'sub'> & { refreshTokenId: string }>(refreshTokenDto.refreshToken, {
1196
- secret,
1197
- audience,
1198
- issuer,
1199
- });
1200
- // const user = await this.userRepository.findOneByOrFail({ id: sub });
1201
- const user = await this.userRepository.findOne({
1202
- where: {
1203
- id: sub,
1204
- },
1205
- relations: {
1206
- roles: true
1207
- }
1208
- });
1209
- if (!user) {
1210
- throw new UnauthorizedException(ERROR_MESSAGES.SESSION_INVALID);
1211
- }
1212
-
1213
- // TODO: Replace the if else condition below with a call to validateAndRotate - Done
1214
- // const isValid = await this.refreshTokenIdsStorage.validate(user.id, refreshTokenId);
1215
- // if (isValid) {
1216
- // // Refresh token rotation.
1217
- // await this.refreshTokenIdsStorage.invalidate(user.id);
1218
- // } else {
1219
- // throw new Error('Refresh token is invalid');
1220
- // }
1221
-
1222
- const currentRefreshToken = await this.refreshTokenIdsStorage.validateAndRotate(user, refreshTokenDto.refreshToken);
1223
-
1224
- await this.userActivityHistoryService.logEvent('tokenRefreshed', user);
1225
-
1226
- return {
1227
- accessToken: await this.generateAccessToken(user),
1228
- refreshToken: currentRefreshToken,
1229
- };
1230
- } catch (err) {
1231
- if (err instanceof InvalidatedRefreshTokenError) {
1232
- // Take action: notify user that his refresh token might have been stolen?
1233
- throw new UnauthorizedException(ERROR_MESSAGES.ACCESS_DENIED);
1234
- }
1235
-
1236
- throw new UnauthorizedException(ERROR_MESSAGES.SESSION_EXPIRED);
1237
- }
1238
- }
1239
-
1240
- private async signToken<T>(userId: number, expiresIn: number, payload?: T) {
1241
- const audience = this.settingService.getConfigValue<SolidCoreSetting>("audience");
1242
- const issuer = this.settingService.getConfigValue<SolidCoreSetting>("issuer");
1243
- const secret = this.settingService.getConfigValue<SolidCoreSetting>("secret");
1244
-
1245
-
1246
- return await this.jwtService.signAsync(
1247
- {
1248
- sub: userId,
1249
- ...payload,
1250
- },
1251
- {
1252
- audience,
1253
- issuer,
1254
- secret,
1255
- expiresIn,
1256
- },
1257
- );
1258
- }
1259
-
1260
- // PROVIDER SPECIFIC CODE
1261
- async validateUserUsingGoogle(user: User) {
1262
- try {
1263
- // Make API call to Google OAuth service to fetch user profile
1264
- const response = await this.httpService.axiosRef.get(`https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${user.googleAccessToken}`);
1265
- const userProfile = response.data;
1266
-
1267
- // Ensure the fetched profile email & provider Id match the ones we have stored in the database earlier.
1268
- if (userProfile.email === user.email && userProfile.id === user.googleId) {
1269
- // TODO: remove the access code both from the database.
1270
- return userProfile;
1271
- } else {
1272
- throw new UnauthorizedException(ERROR_MESSAGES.INVALID_USER_PROFILE);
1273
- }
1274
- } catch (error) {
1275
- throw new UnauthorizedException(ERROR_MESSAGES.GOOGLE_OAUTH_PROFILE_FETCH_FAILED);
1276
- }
1277
- }
1278
-
1279
- async signInUsingGoogle(accessCode: string) {
1280
- const user = await this.userRepository.findOne({
1281
- where: {
1282
- accessCode: accessCode
1283
- },
1284
- relations: {
1285
- roles: true
1286
- }
1287
- });
1288
-
1289
- if (!user) {
1290
- throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND);
1291
- }
1292
- this.checkAccountBlocked(user);
1293
-
1294
- try {
1295
- await this.validateUserUsingGoogle(user);
1296
- } catch (e) {
1297
- await this.incrementFailedAttempts(user);
1298
- throw e;
1299
- }
1300
-
1301
- await this.resetFailedAttempts(user);
1302
- const tokens = await this.generateTokens(user);
1303
- return {
1304
- user: {
1305
- email: user.email,
1306
- mobile: user.mobile,
1307
- username: user.username,
1308
- // forcePasswordChange: user.forcePasswordChange,
1309
- id: user.id,
1310
- roles: user.roles.map((role) => role.name)
1311
- },
1312
- ...tokens
1313
- }
1314
- }
1315
-
1316
- private async isPasswordlessRegistrationEnabled() {
1317
- // return this.settingService.getConfigValue<SolidCoreSetting>('passwordlessRegistration');
1318
- return this.settingService.getConfigValue<SolidCoreSetting>('passwordLessAuth');
1319
- }
1320
-
1321
- private checkAccountBlocked(user: User): void {
1322
- const maxFailedAttempts = this.settingService.getConfigValue<SolidCoreSetting>('maxFailedLoginAttempts') as number;
1323
- if (maxFailedAttempts > 0 && user.failedLoginAttempts >= maxFailedAttempts) {
1324
- throw new ForbiddenException(ERROR_MESSAGES.ACCOUNT_BLOCKED);
1325
- }
1326
- }
1327
-
1328
- private async incrementFailedAttempts(user: User): Promise<void> {
1329
- const nextFailedAttempts = (user.failedLoginAttempts ?? 0) + 1;
1330
- user.failedLoginAttempts = nextFailedAttempts;
1331
- await this.userRepository.update(user.id, { failedLoginAttempts: nextFailedAttempts });
1332
- }
1333
-
1334
- private async resetFailedAttempts(user: User): Promise<void> {
1335
- if (user.failedLoginAttempts === 0) return;
1336
- user.failedLoginAttempts = 0;
1337
- await this.userRepository.update(user.id, { failedLoginAttempts: 0 });
1338
- }
1339
-
1340
- //FIXME - Pending implementation
1341
- // async logout() {
1342
- // // const user = this.request.user; //TODO: // Access the user from the execution context
1343
-
1344
- // // Invalidate the refresh token
1345
- // // await this.refreshTokenIdsStorage.invalidate(user.id);
1346
- // }
1347
- async logout(refreshToken: string) {
1348
- try {
1349
- // const activeUser = this.requestContextService.getActiveUser();
1350
- // const userId = activeUser?.sub;
1351
- // const user = await this.userRepository.findOne({
1352
- // where: {
1353
- // id: userId,
1354
- // }
1355
- // })
1356
- // // Invalidate refresh token if you store them
1357
- // await this.refreshTokenIdsStorage.invalidate(userId); // ← Your existing logic
1358
- // if (!refreshToken) {
1359
- // throw new UnauthorizedException('Refresh token is required');
1360
- // }
1361
- const payload = this.jwtService.decode(refreshToken) as any;
1362
-
1363
- if (!payload || !payload.sub) {
1364
- throw new UnauthorizedException(ERROR_MESSAGES.INVALID_REFRESH_TOKEN);
1365
- }
1366
-
1367
- const userId = payload.sub;
1368
- await this.refreshTokenIdsStorage.invalidate(userId);
1369
- const user = await this.userRepository.findOne({
1370
- where: {
1371
- id: userId,
1372
- }
1373
- })
1374
- // Log logout event
1375
- await this.userActivityHistoryService.logEvent('logout', user);
1376
-
1377
- return { message: SUCCESS_MESSAGES.LOGOUT_SUCCESS };
1378
- } catch (err) {
1379
- throw err instanceof UnauthorizedException || err instanceof InternalServerErrorException
1380
- ? err
1381
- : new InternalServerErrorException(ERROR_MESSAGES.LOGOUT_FAILED);
1382
- }
1383
- }
1384
-
1385
-
1386
- async activateUser(userId: number) {
1387
- const user = await this.userService.findOne(userId, {});
1388
- if (!user) {
1389
- throw new NotFoundException(ERROR_MESSAGES.USER_NOT_FOUND);
1390
- }
1391
- user.active = true;
1392
- await this.userRepository.save(user);
1393
- }
1394
-
1395
- async me(activeUser: ActiveUserData) {
1396
- const user = await this.userRepository.findOne({
1397
- where: {
1398
- id: activeUser.sub,
1399
- },
1400
- relations: {
1401
- roles: true
1402
- }
1403
- });
1404
-
1405
- // const tokens = await this.generateTokens(user);
1406
-
1407
- // Get the refresh token for a user from refresh token storage.
1408
- const refreshTokenState = await this.refreshTokenIdsStorage.getCurrentRefreshTokenState(user.id);
1409
-
1410
- const response = {
1411
- user: {
1412
- email: user.email,
1413
- mobile: user.mobile,
1414
- username: user.username,
1415
- // forcePasswordChange: user.forcePasswordChange,
1416
- id: user.id,
1417
- roles: user.roles.map((role) => role.name)
1418
- },
1419
- refreshToken: refreshTokenState.currentRefreshToken,
1420
- // ...tokens
1421
- }
1422
- return response;
1423
- }
1424
-
1425
- async generateSsoCode(activeUser: ActiveUserData, rawAccessToken: string): Promise<{ ssoCode: string }> {
1426
- const refreshTokenState = await this.refreshTokenIdsStorage.getCurrentRefreshTokenState(activeUser.sub);
1427
- if (!refreshTokenState?.currentRefreshToken) {
1428
- throw new UnauthorizedException('No active session found');
1429
- }
1430
- const ssoCode = await this.ssoCodeStorage.generateCode(
1431
- activeUser.sub,
1432
- rawAccessToken,
1433
- refreshTokenState.currentRefreshToken,
1354
+ .where("id = :id", { id: user.id })
1355
+ .andWhere("verificationTokenOnForgotPassword = :token", {
1356
+ token: confirmForgotPasswordDto.verificationToken,
1357
+ })
1358
+ .andWhere("verificationTokenOnForgotPasswordExpiresAt > NOW()")
1359
+ .execute();
1360
+
1361
+ if (affected !== 1) {
1362
+ // Token invalid/expired/already used (or a parallel call already consumed it)
1363
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1364
+ }
1365
+
1366
+ // 2) Now update the password & history (still inside the same transaction)
1367
+ const pwdHash = await this.hashingService.hash(
1368
+ confirmForgotPasswordDto.password,
1369
+ );
1370
+ const pwdScheme = this.hashingService.name(); // e.g. bcrypt
1371
+ const pwdSchemeVersion = this.hashingService.currentVersion(); // e.g. 1, 2, 3 ...
1372
+
1373
+ // Check reuse with your existing method (ensure it looks at hashes).
1374
+ await m.getRepository(User).update(
1375
+ { id: user.id },
1376
+ {
1377
+ password: pwdHash,
1378
+ passwordScheme: pwdScheme,
1379
+ passwordSchemeVersion: pwdSchemeVersion,
1380
+ },
1381
+ );
1382
+ await this.notifyUserOnPasswordChanged(user);
1383
+
1384
+ return {
1385
+ status: "success",
1386
+ message: SUCCESS_MESSAGES.FORGOT_PASSWORD_CONFIRMED,
1387
+ error: "",
1388
+ errorCode: "",
1389
+ data: {},
1390
+ };
1391
+ });
1392
+ }
1393
+
1394
+ private async notifyUserOnPasswordChanged(user: User) {
1395
+ const companyLogo = await this.getCompanyLogo();
1396
+ const forgotPasswordSendVerificationTokenOn =
1397
+ this.settingService.getConfigValue<SolidCoreSetting>(
1398
+ "forgotPasswordSendVerificationTokenOn",
1399
+ );
1400
+
1401
+ if (
1402
+ forgotPasswordSendVerificationTokenOn ==
1403
+ ForgotPasswordSendVerificationTokenOn.EMAIL
1404
+ ) {
1405
+ const mailService = this.mailServiceFactory.getMailService();
1406
+ mailService.sendEmailUsingTemplate(
1407
+ user.email,
1408
+ "password-changed",
1409
+ {
1410
+ solidAppName:
1411
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
1412
+ solidAppWebsiteUrl:
1413
+ this.settingService.getConfigValue<SolidCoreSetting>(
1414
+ "solidAppWebsiteUrl",
1415
+ ),
1416
+ email: user.email,
1417
+ firstName: user.username,
1418
+ fullName: user.fullName,
1419
+ // TODO: Need to prefix this with the page url where the forgot password page will open up.
1420
+ passwordResetLink: `${this.settingService.getConfigValue<SolidCoreSetting>("frontendForgotPasswordPageUrl")}?token=${user.verificationTokenOnForgotPassword}`,
1421
+ companyLogoUrl: companyLogo,
1422
+ },
1423
+ this.settingService.getConfigValue<SolidCoreSetting>(
1424
+ "shouldQueueEmails",
1425
+ ),
1426
+ null,
1427
+ null,
1428
+ "user",
1429
+ user.id,
1430
+ );
1431
+ }
1432
+ // Assuming all users do not have mobile as mandatory.
1433
+ if (
1434
+ forgotPasswordSendVerificationTokenOn ==
1435
+ ForgotPasswordSendVerificationTokenOn.MOBILE &&
1436
+ user.mobile
1437
+ ) {
1438
+ const smsService = this.smsFactory.getSmsService();
1439
+ smsService.sendSMSUsingTemplate(
1440
+ user.mobile,
1441
+ "forgot-password",
1442
+ {
1443
+ solidAppName:
1444
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
1445
+ otp: user.verificationTokenOnForgotPassword,
1446
+ verificationTokenOnForgotPassword:
1447
+ user.verificationTokenOnForgotPassword,
1448
+ firstName: user.username,
1449
+ companyLogoUrl: companyLogo,
1450
+ },
1451
+ this.settingService.getConfigValue<SolidCoreSetting>("shouldQueueSms"),
1452
+ );
1453
+ }
1454
+ }
1455
+
1456
+ async generateTokens(user: User) {
1457
+ const [accessToken, refreshToken] = await Promise.all([
1458
+ await this.generateAccessToken(user),
1459
+ await this.generateRefreshToken(user),
1460
+ ]);
1461
+
1462
+ return {
1463
+ accessToken,
1464
+ refreshToken,
1465
+ };
1466
+ }
1467
+
1468
+ async generateAccessToken(user: User) {
1469
+ // const userRoleNames = user.roles.map((role) => role.name).join(';')
1470
+ const userRoleNames = user.roles.map((role) => role.name);
1471
+
1472
+ const accessTokenTtl =
1473
+ this.settingService.getConfigValue<SolidCoreSetting>("accessTokenTtl");
1474
+ const accessToken = await this.signToken<Partial<ActiveUserData>>(
1475
+ user.id,
1476
+ accessTokenTtl,
1477
+ { username: user.username, email: user.email, roles: userRoleNames },
1478
+ );
1479
+
1480
+ return accessToken;
1481
+ }
1482
+
1483
+ async generateRefreshToken(user: User, previousRefreshToken?: string) {
1484
+ const refreshTokenId = randomUUID();
1485
+ const refreshTokenTtl =
1486
+ this.settingService.getConfigValue<SolidCoreSetting>("refreshTokenTtl");
1487
+ const refreshToken = await this.signToken(user.id, refreshTokenTtl, {
1488
+ refreshTokenId,
1489
+ });
1490
+
1491
+ // store the refresh token id in the redis storage.
1492
+ await this.refreshTokenIdsStorage.insert(
1493
+ user.id,
1494
+ refreshToken,
1495
+ previousRefreshToken,
1496
+ );
1497
+
1498
+ return refreshToken;
1499
+ }
1500
+
1501
+ async refreshTokens(refreshTokenDto: RefreshTokenDto) {
1502
+ try {
1503
+ const secret =
1504
+ this.settingService.getConfigValue<SolidCoreSetting>("secret");
1505
+ const audience =
1506
+ this.settingService.getConfigValue<SolidCoreSetting>("audience");
1507
+ const issuer =
1508
+ this.settingService.getConfigValue<SolidCoreSetting>("issuer");
1509
+
1510
+ const { sub } = await this.jwtService.verifyAsync<
1511
+ Pick<ActiveUserData, "sub"> & { refreshTokenId: string }
1512
+ >(refreshTokenDto.refreshToken, {
1513
+ secret,
1514
+ audience,
1515
+ issuer,
1516
+ });
1517
+ // const user = await this.userRepository.findOneByOrFail({ id: sub });
1518
+ const user = await this.userRepository.findOne({
1519
+ where: {
1520
+ id: sub,
1521
+ },
1522
+ relations: {
1523
+ roles: true,
1524
+ },
1525
+ });
1526
+ if (!user) {
1527
+ throw new UnauthorizedException(ERROR_MESSAGES.SESSION_INVALID);
1528
+ }
1529
+
1530
+ // TODO: Replace the if else condition below with a call to validateAndRotate - Done
1531
+ // const isValid = await this.refreshTokenIdsStorage.validate(user.id, refreshTokenId);
1532
+ // if (isValid) {
1533
+ // // Refresh token rotation.
1534
+ // await this.refreshTokenIdsStorage.invalidate(user.id);
1535
+ // } else {
1536
+ // throw new Error('Refresh token is invalid');
1537
+ // }
1538
+
1539
+ const currentRefreshToken =
1540
+ await this.refreshTokenIdsStorage.validateAndRotate(
1541
+ user,
1542
+ refreshTokenDto.refreshToken,
1434
1543
  );
1435
- return { ssoCode };
1436
- }
1437
-
1438
- async exchangeSsoCode(code: string) {
1439
- const { userId, accessToken, refreshToken } = await this.ssoCodeStorage.consumeCode(code);
1440
- const user = await this.userRepository.findOne({ where: { id: userId }, relations: { roles: true } });
1441
- if (!user) {
1442
- throw new UnauthorizedException('User not found');
1443
- }
1444
- return { accessToken, refreshToken, user: this.buildUserPayload(user) };
1445
- }
1446
1544
 
1545
+ await this.userActivityHistoryService.logEvent("tokenRefreshed", user);
1546
+
1547
+ return {
1548
+ accessToken: await this.generateAccessToken(user),
1549
+ refreshToken: currentRefreshToken,
1550
+ };
1551
+ } catch (err) {
1552
+ if (err instanceof InvalidatedRefreshTokenError) {
1553
+ // Take action: notify user that his refresh token might have been stolen?
1554
+ throw new UnauthorizedException(ERROR_MESSAGES.ACCESS_DENIED);
1555
+ }
1556
+
1557
+ throw new UnauthorizedException(ERROR_MESSAGES.SESSION_EXPIRED);
1558
+ }
1559
+ }
1560
+
1561
+ private async signToken<T>(userId: number, expiresIn: number, payload?: T) {
1562
+ const audience =
1563
+ this.settingService.getConfigValue<SolidCoreSetting>("audience");
1564
+ const issuer =
1565
+ this.settingService.getConfigValue<SolidCoreSetting>("issuer");
1566
+ const secret =
1567
+ this.settingService.getConfigValue<SolidCoreSetting>("secret");
1568
+
1569
+ return await this.jwtService.signAsync(
1570
+ {
1571
+ sub: userId,
1572
+ ...payload,
1573
+ },
1574
+ {
1575
+ audience,
1576
+ issuer,
1577
+ secret,
1578
+ expiresIn,
1579
+ },
1580
+ );
1581
+ }
1582
+
1583
+ // PROVIDER SPECIFIC CODE
1584
+ async validateUserUsingGoogle(user: User) {
1585
+ try {
1586
+ // Make API call to Google OAuth service to fetch user profile
1587
+ const response = await this.httpService.axiosRef.get(
1588
+ `https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${user.googleAccessToken}`,
1589
+ );
1590
+ const userProfile = response.data;
1591
+
1592
+ // Ensure the fetched profile email & provider Id match the ones we have stored in the database earlier.
1593
+ if (
1594
+ userProfile.email === user.email &&
1595
+ userProfile.id === user.googleId
1596
+ ) {
1597
+ // TODO: remove the access code both from the database.
1598
+ return userProfile;
1599
+ } else {
1600
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_USER_PROFILE);
1601
+ }
1602
+ } catch (error) {
1603
+ throw new UnauthorizedException(
1604
+ ERROR_MESSAGES.GOOGLE_OAUTH_PROFILE_FETCH_FAILED,
1605
+ );
1606
+ }
1607
+ }
1608
+
1609
+ async signInUsingGoogle(accessCode: string) {
1610
+ const user = await this.userRepository.findOne({
1611
+ where: {
1612
+ accessCode: accessCode,
1613
+ },
1614
+ relations: {
1615
+ roles: true,
1616
+ },
1617
+ });
1618
+
1619
+ if (!user) {
1620
+ throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND);
1621
+ }
1622
+ this.checkAccountBlocked(user);
1623
+
1624
+ try {
1625
+ await this.validateUserUsingGoogle(user);
1626
+ } catch (e) {
1627
+ await this.incrementFailedAttempts(user);
1628
+ throw e;
1629
+ }
1630
+
1631
+ await this.resetFailedAttempts(user);
1632
+ const tokens = await this.generateTokens(user);
1633
+ return {
1634
+ user: {
1635
+ email: user.email,
1636
+ mobile: user.mobile,
1637
+ username: user.username,
1638
+ // forcePasswordChange: user.forcePasswordChange,
1639
+ id: user.id,
1640
+ roles: user.roles.map((role) => role.name),
1641
+ },
1642
+ ...tokens,
1643
+ };
1644
+ }
1645
+
1646
+ async validateUserUsingFacebook(user: User) {
1647
+ if (!user.facebookAccessToken || !user.facebookId) {
1648
+ throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND);
1649
+ }
1650
+
1651
+ try {
1652
+ const response = await this.httpService.axiosRef.get(
1653
+ `https://graph.facebook.com/me`,
1654
+ {
1655
+ params: { fields: "id,name,email" },
1656
+ headers: {
1657
+ Authorization: `Bearer ${user.facebookAccessToken}`,
1658
+ },
1659
+ },
1660
+ );
1661
+ const userProfile = response.data;
1662
+
1663
+ if (
1664
+ userProfile.id === user.facebookId &&
1665
+ (!user.email || !userProfile.email || userProfile.email === user.email)
1666
+ ) {
1667
+ return userProfile;
1668
+ } else {
1669
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_USER_PROFILE);
1670
+ }
1671
+ } catch (error) {
1672
+ if (error instanceof UnauthorizedException) {
1673
+ throw error;
1674
+ }
1675
+ throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND);
1676
+ }
1677
+ }
1678
+
1679
+ async signInUsingFacebook(accessCode: string) {
1680
+ const user = await this.userRepository.findOne({
1681
+ where: {
1682
+ accessCode: accessCode,
1683
+ },
1684
+ relations: {
1685
+ roles: true,
1686
+ },
1687
+ });
1688
+
1689
+ if (!user) {
1690
+ throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND);
1691
+ }
1692
+ this.checkAccountBlocked(user);
1693
+
1694
+ try {
1695
+ await this.validateUserUsingFacebook(user);
1696
+ } catch (e) {
1697
+ await this.incrementFailedAttempts(user);
1698
+ throw e;
1699
+ }
1700
+
1701
+ await this.resetFailedAttempts(user);
1702
+ const tokens = await this.generateTokens(user);
1703
+ return {
1704
+ user: {
1705
+ email: user.email,
1706
+ mobile: user.mobile,
1707
+ username: user.username,
1708
+ id: user.id,
1709
+ roles: user.roles.map((role) => role.name),
1710
+ },
1711
+ ...tokens,
1712
+ };
1713
+ }
1714
+
1715
+ async validateUserUsingMicrosoft(user: User) {
1716
+ try {
1717
+ const response = await this.httpService.axiosRef.get(
1718
+ `https://graph.microsoft.com/v1.0/me`,
1719
+ {
1720
+ headers: {
1721
+ Authorization: `Bearer ${user.microsoftAccessToken}`,
1722
+ },
1723
+ },
1724
+ );
1725
+ const userProfile = response.data;
1726
+ const profileEmail = userProfile.mail || userProfile.userPrincipalName;
1727
+
1728
+ if (
1729
+ userProfile.id === user.microsoftId &&
1730
+ (!user.email || profileEmail === user.email)
1731
+ ) {
1732
+ return userProfile;
1733
+ } else {
1734
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_USER_PROFILE);
1735
+ }
1736
+ } catch (error) {
1737
+ throw new UnauthorizedException("Microsoft OAuth profile fetch failed");
1738
+ }
1739
+ }
1740
+
1741
+ async signInUsingMicrosoft(accessCode: string) {
1742
+ const user = await this.userRepository.findOne({
1743
+ where: {
1744
+ accessCode: accessCode,
1745
+ },
1746
+ relations: {
1747
+ roles: true,
1748
+ },
1749
+ });
1750
+
1751
+ if (!user) {
1752
+ throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND);
1753
+ }
1754
+ this.checkAccountBlocked(user);
1755
+
1756
+ try {
1757
+ await this.validateUserUsingMicrosoft(user);
1758
+ } catch (e) {
1759
+ await this.incrementFailedAttempts(user);
1760
+ throw e;
1761
+ }
1762
+
1763
+ await this.resetFailedAttempts(user);
1764
+ const tokens = await this.generateTokens(user);
1765
+ return {
1766
+ user: {
1767
+ email: user.email,
1768
+ mobile: user.mobile,
1769
+ username: user.username,
1770
+ id: user.id,
1771
+ roles: user.roles.map((role) => role.name),
1772
+ },
1773
+ ...tokens,
1774
+ };
1775
+ }
1776
+
1777
+ async signInUsingApple(accessCode: string) {
1778
+ const user = await this.userRepository.findOne({
1779
+ where: {
1780
+ accessCode: accessCode,
1781
+ },
1782
+ relations: {
1783
+ roles: true,
1784
+ },
1785
+ });
1786
+
1787
+ if (!user) {
1788
+ throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND);
1789
+ }
1790
+ this.checkAccountBlocked(user);
1791
+
1792
+ // Apple OAuth token validation is typically handled by the strategy itself
1793
+ // as it involves verifying the JWT idToken.
1794
+ // If we want to re-verify here, we'd need to verify the appleAccessToken or idToken.
1795
+ // For now, we follow the pattern of trust in the accessCode generated after successful strategy validation.
1796
+
1797
+ await this.resetFailedAttempts(user);
1798
+ const tokens = await this.generateTokens(user);
1799
+ return {
1800
+ user: {
1801
+ email: user.email,
1802
+ mobile: user.mobile,
1803
+ username: user.username,
1804
+ id: user.id,
1805
+ roles: user.roles.map((role) => role.name),
1806
+ },
1807
+ ...tokens,
1808
+ };
1809
+ }
1810
+
1811
+ private async isPasswordlessRegistrationEnabled() {
1812
+ // return this.settingService.getConfigValue<SolidCoreSetting>('passwordlessRegistration');
1813
+ return this.settingService.getConfigValue<SolidCoreSetting>(
1814
+ "passwordLessAuth",
1815
+ );
1816
+ }
1817
+
1818
+ private checkAccountBlocked(user: User): void {
1819
+ const maxFailedAttempts =
1820
+ this.settingService.getConfigValue<SolidCoreSetting>(
1821
+ "maxFailedLoginAttempts",
1822
+ ) as number;
1823
+ if (
1824
+ maxFailedAttempts > 0 &&
1825
+ user.failedLoginAttempts >= maxFailedAttempts
1826
+ ) {
1827
+ throw new ForbiddenException(ERROR_MESSAGES.ACCOUNT_BLOCKED);
1828
+ }
1829
+ }
1830
+
1831
+ private async incrementFailedAttempts(user: User): Promise<void> {
1832
+ const nextFailedAttempts = (user.failedLoginAttempts ?? 0) + 1;
1833
+ user.failedLoginAttempts = nextFailedAttempts;
1834
+ await this.userRepository.update(user.id, {
1835
+ failedLoginAttempts: nextFailedAttempts,
1836
+ });
1837
+ }
1838
+
1839
+ private async resetFailedAttempts(user: User): Promise<void> {
1840
+ if (user.failedLoginAttempts === 0) return;
1841
+ user.failedLoginAttempts = 0;
1842
+ await this.userRepository.update(user.id, { failedLoginAttempts: 0 });
1843
+ }
1844
+
1845
+ //FIXME - Pending implementation
1846
+ // async logout() {
1847
+ // // const user = this.request.user; //TODO: // Access the user from the execution context
1848
+
1849
+ // // Invalidate the refresh token
1850
+ // // await this.refreshTokenIdsStorage.invalidate(user.id);
1851
+ // }
1852
+ async logout(refreshToken: string) {
1853
+ try {
1854
+ // const activeUser = this.requestContextService.getActiveUser();
1855
+ // const userId = activeUser?.sub;
1856
+ // const user = await this.userRepository.findOne({
1857
+ // where: {
1858
+ // id: userId,
1859
+ // }
1860
+ // })
1861
+ // // Invalidate refresh token if you store them
1862
+ // await this.refreshTokenIdsStorage.invalidate(userId); // ← Your existing logic
1863
+ // if (!refreshToken) {
1864
+ // throw new UnauthorizedException('Refresh token is required');
1865
+ // }
1866
+ const payload = this.jwtService.decode(refreshToken) as any;
1867
+
1868
+ if (!payload || !payload.sub) {
1869
+ throw new UnauthorizedException(ERROR_MESSAGES.INVALID_REFRESH_TOKEN);
1870
+ }
1871
+
1872
+ const userId = payload.sub;
1873
+ await this.refreshTokenIdsStorage.invalidate(userId);
1874
+ const user = await this.userRepository.findOne({
1875
+ where: {
1876
+ id: userId,
1877
+ },
1878
+ });
1879
+ // Log logout event
1880
+ await this.userActivityHistoryService.logEvent("logout", user);
1881
+
1882
+ return { message: SUCCESS_MESSAGES.LOGOUT_SUCCESS };
1883
+ } catch (err) {
1884
+ throw err instanceof UnauthorizedException ||
1885
+ err instanceof InternalServerErrorException
1886
+ ? err
1887
+ : new InternalServerErrorException(ERROR_MESSAGES.LOGOUT_FAILED);
1888
+ }
1889
+ }
1890
+
1891
+ async activateUser(userId: number) {
1892
+ const user = await this.userService.findOne(userId, {});
1893
+ if (!user) {
1894
+ throw new NotFoundException(ERROR_MESSAGES.USER_NOT_FOUND);
1895
+ }
1896
+ user.active = true;
1897
+ await this.userRepository.save(user);
1898
+ }
1899
+
1900
+ async me(activeUser: ActiveUserData) {
1901
+ const user = await this.userRepository.findOne({
1902
+ where: {
1903
+ id: activeUser.sub,
1904
+ },
1905
+ relations: {
1906
+ roles: true,
1907
+ },
1908
+ });
1909
+
1910
+ // const tokens = await this.generateTokens(user);
1911
+
1912
+ // Get the refresh token for a user from refresh token storage.
1913
+ const refreshTokenState =
1914
+ await this.refreshTokenIdsStorage.getCurrentRefreshTokenState(user.id);
1915
+
1916
+ const response = {
1917
+ user: {
1918
+ email: user.email,
1919
+ mobile: user.mobile,
1920
+ username: user.username,
1921
+ // forcePasswordChange: user.forcePasswordChange,
1922
+ id: user.id,
1923
+ roles: user.roles.map((role) => role.name),
1924
+ },
1925
+ refreshToken: refreshTokenState.currentRefreshToken,
1926
+ // ...tokens
1927
+ };
1928
+ return response;
1929
+ }
1930
+
1931
+ async generateSsoCode(
1932
+ activeUser: ActiveUserData,
1933
+ rawAccessToken: string,
1934
+ ): Promise<{ ssoCode: string }> {
1935
+ const refreshTokenState =
1936
+ await this.refreshTokenIdsStorage.getCurrentRefreshTokenState(
1937
+ activeUser.sub,
1938
+ );
1939
+ if (!refreshTokenState?.currentRefreshToken) {
1940
+ throw new UnauthorizedException("No active session found");
1941
+ }
1942
+ const ssoCode = await this.ssoCodeStorage.generateCode(
1943
+ activeUser.sub,
1944
+ rawAccessToken,
1945
+ refreshTokenState.currentRefreshToken,
1946
+ );
1947
+ return { ssoCode };
1948
+ }
1949
+
1950
+ async exchangeSsoCode(code: string) {
1951
+ const { userId, accessToken, refreshToken } =
1952
+ await this.ssoCodeStorage.consumeCode(code);
1953
+ const user = await this.userRepository.findOne({
1954
+ where: { id: userId },
1955
+ relations: { roles: true },
1956
+ });
1957
+ if (!user) {
1958
+ throw new UnauthorizedException("User not found");
1959
+ }
1960
+ return { accessToken, refreshToken, user: this.buildUserPayload(user) };
1961
+ }
1447
1962
  }
1448
1963
 
1449
1964
  function parseUniqueConstraintError(detail: string): string {
1450
- const match = detail.match(/Key \(([^)]+)\)=\(([^)]+)\) already exists\./);
1451
- if (match) {
1452
- const field = match[1];
1453
- const value = match[2];
1454
- const fieldMap: Record<string, string> = {
1455
- username: 'username',
1456
- email: 'email address',
1457
- full_name_user_key: 'full name',
1458
- };
1459
- const friendlyField = fieldMap[field] || field;
1460
- return `A user with ${friendlyField} "${value}" already exists.`;
1461
- }
1462
- return detail;
1965
+ const match = detail.match(/Key \(([^)]+)\)=\(([^)]+)\) already exists\./);
1966
+ if (match) {
1967
+ const field = match[1];
1968
+ const value = match[2];
1969
+ const fieldMap: Record<string, string> = {
1970
+ username: "username",
1971
+ email: "email address",
1972
+ full_name_user_key: "full name",
1973
+ };
1974
+ const friendlyField = fieldMap[field] || field;
1975
+ return `A user with ${friendlyField} "${value}" already exists.`;
1976
+ }
1977
+ return detail;
1463
1978
  }