@solidxai/core 0.1.10-beta.15 → 0.1.10-beta.21

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 (116) hide show
  1. package/dist/commands/test-data.command.d.ts +2 -0
  2. package/dist/commands/test-data.command.d.ts.map +1 -1
  3. package/dist/commands/test-data.command.js +32 -6
  4. package/dist/commands/test-data.command.js.map +1 -1
  5. package/dist/controllers/chatter-message.controller.d.ts +2 -0
  6. package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
  7. package/dist/controllers/chatter-message.controller.js +17 -0
  8. package/dist/controllers/chatter-message.controller.js.map +1 -1
  9. package/dist/controllers/gupshup-webhook.controller.d.ts +11 -0
  10. package/dist/controllers/gupshup-webhook.controller.d.ts.map +1 -0
  11. package/dist/controllers/gupshup-webhook.controller.js +87 -0
  12. package/dist/controllers/gupshup-webhook.controller.js.map +1 -0
  13. package/dist/controllers/meta-cloud-whatsapp-webhook.controller.d.ts +16 -0
  14. package/dist/controllers/meta-cloud-whatsapp-webhook.controller.d.ts.map +1 -0
  15. package/dist/controllers/meta-cloud-whatsapp-webhook.controller.js +133 -0
  16. package/dist/controllers/meta-cloud-whatsapp-webhook.controller.js.map +1 -0
  17. package/dist/dtos/update-chatter-note-message.dto.d.ts +5 -0
  18. package/dist/dtos/update-chatter-note-message.dto.d.ts.map +1 -0
  19. package/dist/dtos/update-chatter-note-message.dto.js +34 -0
  20. package/dist/dtos/update-chatter-note-message.dto.js.map +1 -0
  21. package/dist/factories/whatsapp.factory.d.ts.map +1 -1
  22. package/dist/factories/whatsapp.factory.js +13 -11
  23. package/dist/factories/whatsapp.factory.js.map +1 -1
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/interfaces.d.ts +1 -0
  29. package/dist/interfaces.d.ts.map +1 -1
  30. package/dist/interfaces.js.map +1 -1
  31. package/dist/listeners/user-registration.listener.d.ts +5 -2
  32. package/dist/listeners/user-registration.listener.d.ts.map +1 -1
  33. package/dist/listeners/user-registration.listener.js +37 -4
  34. package/dist/listeners/user-registration.listener.js.map +1 -1
  35. package/dist/seeders/module-test-data.service.d.ts +2 -0
  36. package/dist/seeders/module-test-data.service.d.ts.map +1 -1
  37. package/dist/seeders/module-test-data.service.js +61 -0
  38. package/dist/seeders/module-test-data.service.js.map +1 -1
  39. package/dist/seeders/seed-data/solid-core-metadata.json +1 -0
  40. package/dist/services/authentication.service.d.ts +6 -1
  41. package/dist/services/authentication.service.d.ts.map +1 -1
  42. package/dist/services/authentication.service.js +144 -14
  43. package/dist/services/authentication.service.js.map +1 -1
  44. package/dist/services/chatter-message.service.d.ts +7 -1
  45. package/dist/services/chatter-message.service.d.ts.map +1 -1
  46. package/dist/services/chatter-message.service.js +94 -2
  47. package/dist/services/chatter-message.service.js.map +1 -1
  48. package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts +1 -0
  49. package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts.map +1 -1
  50. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +11 -0
  51. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
  52. package/dist/services/mediaStorageProviders/file-storage-provider.d.ts +1 -0
  53. package/dist/services/mediaStorageProviders/file-storage-provider.d.ts.map +1 -1
  54. package/dist/services/mediaStorageProviders/file-storage-provider.js +6 -0
  55. package/dist/services/mediaStorageProviders/file-storage-provider.js.map +1 -1
  56. package/dist/services/settings/default-settings-provider.service.d.ts +174 -0
  57. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  58. package/dist/services/settings/default-settings-provider.service.js +98 -0
  59. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  60. package/dist/services/user.service.js +1 -1
  61. package/dist/services/user.service.js.map +1 -1
  62. package/dist/services/whatsapp/GupshupOtpWhatsappService.d.ts +11 -0
  63. package/dist/services/whatsapp/GupshupOtpWhatsappService.d.ts.map +1 -0
  64. package/dist/services/whatsapp/GupshupOtpWhatsappService.js +127 -0
  65. package/dist/services/whatsapp/GupshupOtpWhatsappService.js.map +1 -0
  66. package/dist/services/whatsapp/MetaCloudWhatsappService.d.ts +17 -0
  67. package/dist/services/whatsapp/MetaCloudWhatsappService.d.ts.map +1 -0
  68. package/dist/services/whatsapp/MetaCloudWhatsappService.js +209 -0
  69. package/dist/services/whatsapp/MetaCloudWhatsappService.js.map +1 -0
  70. package/dist/solid-core.module.d.ts +2 -2
  71. package/dist/solid-core.module.d.ts.map +1 -1
  72. package/dist/solid-core.module.js +14 -6
  73. package/dist/solid-core.module.js.map +1 -1
  74. package/dist-tests/api/authenticate.spec.js +119 -0
  75. package/dist-tests/api/authenticate.spec.js.map +1 -0
  76. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +97 -0
  77. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +1 -0
  78. package/dist-tests/api/ping.spec.js +21 -0
  79. package/dist-tests/api/ping.spec.js.map +1 -0
  80. package/dist-tests/helpers/auth.js +41 -0
  81. package/dist-tests/helpers/auth.js.map +1 -0
  82. package/dist-tests/helpers/env.js +11 -0
  83. package/dist-tests/helpers/env.js.map +1 -0
  84. package/docs/grouping-enhancements.md +89 -0
  85. package/docs/java-spring/README.md +3 -0
  86. package/docs/java-spring/solid-core-module-deep-dive-report.md +1317 -0
  87. package/docs/seed-changes.md +65 -0
  88. package/docs/test-data-workflow.md +200 -0
  89. package/docs/type-declaration-import-issue.md +24 -0
  90. package/package.json +1 -1
  91. package/src/commands/test-data.command.ts +29 -6
  92. package/src/controllers/chatter-message.controller.ts +12 -0
  93. package/src/controllers/gupshup-webhook.controller.ts +91 -0
  94. package/src/controllers/meta-cloud-whatsapp-webhook.controller.ts +155 -0
  95. package/src/dtos/update-chatter-note-message.dto.ts +14 -0
  96. package/src/factories/whatsapp.factory.ts +33 -32
  97. package/src/helpers/field-crud-managers/field-quality-check-fixes.md +178 -0
  98. package/src/index.ts +2 -0
  99. package/src/interfaces.ts +1 -1
  100. package/src/listeners/user-registration.listener.ts +49 -6
  101. package/src/seeders/module-test-data.service.ts +79 -2
  102. package/src/seeders/seed-data/solid-core-metadata.json +1 -0
  103. package/src/services/authentication.service.ts +281 -46
  104. package/src/services/chatter-message.service.ts +112 -2
  105. package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +12 -0
  106. package/src/services/mediaStorageProviders/file-storage-provider.ts +8 -1
  107. package/src/services/settings/default-settings-provider.service.ts +98 -0
  108. package/src/services/user.service.ts +1 -1
  109. package/src/services/whatsapp/GupshupOtpWhatsappService.ts +174 -0
  110. package/src/services/whatsapp/MetaCloudWhatsappService.ts +253 -0
  111. package/src/solid-core.module.ts +205 -196
  112. package/src/testing/README.md +15 -0
  113. package/.claude/settings.local.json +0 -15
  114. package/CLAUDE.md +0 -18
  115. package/CURRENT_PROMPT +0 -6
  116. package/src/services/1.js +0 -6
@@ -50,6 +50,7 @@ import { SettingService } from "./setting.service";
50
50
  import { UserActivityHistoryService } from "./user-activity-history.service";
51
51
  import { UserService } from "./user.service";
52
52
  import { SmsFactory } from "src/factories/sms.factory";
53
+ import { WhatsAppFactory } from "src/factories/whatsapp.factory";
53
54
  import { SolidRegistry } from "src/helpers/solid-registry";
54
55
 
55
56
  enum LoginProvider {
@@ -79,6 +80,7 @@ export class AuthenticationService {
79
80
  private readonly mailServiceFactory: MailFactory,
80
81
  // private readonly smsService: Msg91OTPService,
81
82
  private readonly smsFactory: SmsFactory,
83
+ private readonly whatsAppFactory: WhatsAppFactory,
82
84
  private readonly eventEmitter: EventEmitter2,
83
85
  private readonly settingService: SettingService,
84
86
  private readonly roleMetadataService: RoleMetadataService,
@@ -193,11 +195,22 @@ export class AuthenticationService {
193
195
  return this.performSignUp(signUpDto, new User(), this.userRepository);
194
196
  }
195
197
 
196
- private async performSignUp<T extends User>(signUpDto: SignUpDto, entity: T, repo: Repository<T>): Promise<T> {
198
+ private async performSignUp<T extends User>(
199
+ signUpDto: SignUpDto,
200
+ entity: T,
201
+ repo: Repository<T>,
202
+ ): Promise<T> {
197
203
  try {
198
- const onForcePasswordChange = this.settingService.getConfigValue<SolidCoreSetting>("forceChangePasswordOnFirstLogin");
199
- const activateUserOnRegistration = this.settingService.getConfigValue<SolidCoreSetting>("activateUserOnRegistration");
200
- const defaultRole = this.settingService.getConfigValue<SolidCoreSetting>("defaultRole");
204
+ const onForcePasswordChange =
205
+ this.settingService.getConfigValue<SolidCoreSetting>(
206
+ "forceChangePasswordOnFirstLogin",
207
+ );
208
+ const activateUserOnRegistration =
209
+ this.settingService.getConfigValue<SolidCoreSetting>(
210
+ "activateUserOnRegistration",
211
+ );
212
+ const defaultRole =
213
+ this.settingService.getConfigValue<SolidCoreSetting>("defaultRole");
201
214
 
202
215
  var { user, pwd, autoGeneratedPwd } = await this.populateForSignup<T>(
203
216
  entity,
@@ -211,14 +224,19 @@ export class AuthenticationService {
211
224
  }
212
225
  const savedUser = await repo.save(user);
213
226
  const userRoles = signUpDto.roles ?? [];
214
- if ((signUpDto.roles?.length ?? 0) === 0 && signUpDto.username !== "sa" && defaultRole) {
227
+ if (
228
+ (signUpDto.roles?.length ?? 0) === 0 &&
229
+ signUpDto.username !== "sa" &&
230
+ defaultRole
231
+ ) {
215
232
  userRoles.push(defaultRole);
216
233
  }
217
234
  await this.handlePostSignup(savedUser, userRoles, pwd, autoGeneratedPwd);
235
+ await this.handlePasswordlessSignupOtp(savedUser, signUpDto, autoGeneratedPwd, repo);
236
+ this.triggerRegistrationEvent(savedUser);
218
237
 
219
238
  return savedUser;
220
- }
221
- catch (err: any) {
239
+ } catch (err: any) {
222
240
  const pgUniqueViolationErrorCode = "23505";
223
241
  if (err.code === pgUniqueViolationErrorCode) {
224
242
  throw new ConflictException(
@@ -231,6 +249,42 @@ export class AuthenticationService {
231
249
  }
232
250
  }
233
251
 
252
+ private async handlePasswordlessSignupOtp<T extends User>(
253
+ user: T,
254
+ signUpDto: SignUpDto,
255
+ autoGeneratedPwd: string,
256
+ repo: Repository<T>,
257
+ ): Promise<void> {
258
+ const isPasswordProvided = !!signUpDto.password;
259
+ const isAutoGeneratedPassword = !!autoGeneratedPwd;
260
+ if (isPasswordProvided || isAutoGeneratedPassword) {
261
+ return;
262
+ }
263
+
264
+ if (!user.mobile) {
265
+ this.logger.warn(
266
+ `Skipping passwordless signup OTP WhatsApp notification for user ${user.username}: mobile is missing.`,
267
+ );
268
+ return;
269
+ }
270
+
271
+ const isPasswordlessRegistrationEnabled =
272
+ await this.isPasswordlessRegistrationEnabled();
273
+ if (!isPasswordlessRegistrationEnabled) {
274
+ return;
275
+ }
276
+
277
+ await this.assignRegistrationOtp(
278
+ PasswordlessRegistrationValidateWhatSources.MOBILE,
279
+ user,
280
+ );
281
+ await repo.save(user);
282
+ await this.notifyUserOnOtpInitiateRegistration(
283
+ user,
284
+ PasswordlessRegistrationValidateWhatSources.MOBILE,
285
+ );
286
+ }
287
+
234
288
  /** @deprecated Use IExtensionUserCreationProvider instead. Kept for backward compatibility. */
235
289
  async signupForExtensionUser<T extends User>(
236
290
  signUpDto: SignUpDto,
@@ -602,7 +656,7 @@ export class AuthenticationService {
602
656
  const companyLogo = await this.getCompanyLogo();
603
657
  if (
604
658
  registrationValidationSource ===
605
- PasswordlessLoginValidateWhatSources.EMAIL
659
+ PasswordlessRegistrationValidateWhatSources.EMAIL
606
660
  ) {
607
661
  const mailService = this.mailServiceFactory.getMailService();
608
662
  mailService.sendEmailUsingTemplate(
@@ -632,25 +686,141 @@ export class AuthenticationService {
632
686
  }
633
687
  if (
634
688
  registrationValidationSource ===
635
- PasswordlessLoginValidateWhatSources.MOBILE
689
+ PasswordlessRegistrationValidateWhatSources.MOBILE
636
690
  ) {
637
- const smsService = this.smsFactory.getSmsService();
638
- smsService.sendSMSUsingTemplate(
691
+ const templateParams = {
692
+ solidAppName:
693
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
694
+ otp: user.mobileVerificationTokenOnRegistration,
695
+ mobileVerificationTokenOnRegistration:
696
+ user.mobileVerificationTokenOnRegistration,
697
+ firstName: user.username,
698
+ fullName: user.fullName ? user.fullName : user.username,
699
+ companyLogoUrl: companyLogo,
700
+ };
701
+
702
+ const whatsappDestination = this.normalizeWhatsAppDestination(
639
703
  user.mobile,
640
- "otp-on-register",
641
- {
642
- solidAppName:
643
- this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
644
- otp: user.mobileVerificationTokenOnRegistration,
645
- mobileVerificationTokenOnRegistration:
646
- user.mobileVerificationTokenOnRegistration,
647
- firstName: user.username,
648
- fullName: user.fullName ? user.fullName : user.username,
649
- companyLogoUrl: companyLogo,
650
- },
651
- this.settingService.getConfigValue<SolidCoreSetting>("shouldQueueSms"),
652
704
  );
705
+ const gupshupTemplateAppName =
706
+ process.env.COMMON_GUPSHUP_APP_NAME || "Gupshup";
707
+ const whatsappTemplateId =
708
+ this.settingService.getConfigValue<SolidCoreSetting>(
709
+ "otpWhatsappTemplateId",
710
+ ) || "common_otp";
711
+ const whatsappIndependentEnabled =
712
+ this.settingService.getConfigValue<SolidCoreSetting>(
713
+ "otpWhatsappIndependentEnabled",
714
+ ) !== false;
715
+
716
+ let smsSent = false;
717
+ let whatsappSent = false;
718
+ let smsErrorMessage: string | undefined;
719
+ let whatsappErrorMessage: string | undefined;
720
+
721
+ this.logger.debug(
722
+ `OTP SMS send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}`,
723
+ );
724
+
725
+ try {
726
+ const smsService = this.smsFactory.getSmsService();
727
+ await smsService.sendSMSUsingTemplate(
728
+ user.mobile,
729
+ "otp-on-register",
730
+ templateParams,
731
+ false,
732
+ );
733
+ smsSent = true;
734
+ } catch (smsError: any) {
735
+ smsErrorMessage = smsError?.message;
736
+ this.logger.warn(
737
+ `OTP SMS failed: destination=${user.mobile}, message=${smsErrorMessage}`,
738
+ );
739
+ }
740
+
741
+ if (whatsappIndependentEnabled) {
742
+ if (!whatsappDestination) {
743
+ whatsappErrorMessage = "Normalized WhatsApp destination is empty";
744
+ this.logger.error(
745
+ `Independent OTP WhatsApp skipped: destination=${user.mobile}, message=${whatsappErrorMessage}`,
746
+ );
747
+ } else {
748
+ this.logger.debug(
749
+ `Independent OTP WhatsApp send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`,
750
+ );
751
+ try {
752
+ await this.sendOtpToWhatsappProvider(
753
+ whatsappDestination,
754
+ String(whatsappTemplateId),
755
+ String(gupshupTemplateAppName),
756
+ String(templateParams.otp || ""),
757
+ );
758
+ whatsappSent = true;
759
+ this.logger.log(
760
+ `Independent OTP WhatsApp success: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`,
761
+ );
762
+ } catch (waError: any) {
763
+ whatsappErrorMessage = waError?.message;
764
+ this.logger.error(
765
+ `Independent OTP WhatsApp failed: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}, message=${whatsappErrorMessage}`,
766
+ );
767
+ }
768
+ }
769
+ }
770
+
771
+ if (!smsSent && !whatsappSent) {
772
+ throw new Error(
773
+ `OTP delivery failed on both channels. smsError=${smsErrorMessage || "unknown"}, whatsappError=${whatsappErrorMessage || "disabled/unknown"}`,
774
+ );
775
+ }
776
+ }
777
+ }
778
+
779
+ private normalizeWhatsAppDestination(rawMobile: string): string {
780
+ const raw = (rawMobile || "").trim();
781
+ let sanitized = raw.replace(/[^\d+]/g, "");
782
+
783
+ if (sanitized.startsWith("00")) {
784
+ sanitized = `+${sanitized.slice(2)}`;
785
+ }
786
+
787
+ const defaultDialCode = String(
788
+ this.settingService.getConfigValue<SolidCoreSetting>(
789
+ "otpDefaultCountryDialCode",
790
+ ) || "",
791
+ ).replace(/\D/g, "");
792
+
793
+ if (sanitized.startsWith("+")) {
794
+ const e164Digits = sanitized.slice(1).replace(/\D/g, "");
795
+ if (e164Digits.length >= 8 && e164Digits.length <= 15) {
796
+ return e164Digits;
797
+ }
798
+ }
799
+
800
+ const digits = sanitized.replace(/\D/g, "");
801
+
802
+ if (digits.length >= 11 && digits.length <= 15) {
803
+ return digits;
653
804
  }
805
+
806
+ if (digits.length === 10 && defaultDialCode) {
807
+ return `${defaultDialCode}${digits}`;
808
+ }
809
+
810
+ return digits;
811
+ }
812
+
813
+ private async sendOtpToWhatsappProvider(
814
+ destination: string,
815
+ templateId: string,
816
+ appName: string,
817
+ otp: string,
818
+ ): Promise<void> {
819
+ const whatsappService = this.whatsAppFactory.getWhatsappService();
820
+ await whatsappService.sendWhatsAppMessage(destination, null, {
821
+ type: "text",
822
+ text: `${appName} OTP is ${otp}. It is valid for 10 mins.`,
823
+ });
654
824
  }
655
825
 
656
826
  async otpConfirmRegistration(confirmSignUpDto: OTPConfirmOTPDto) {
@@ -753,7 +923,7 @@ export class AuthenticationService {
753
923
  this.resolvePasswordlessValidationSource();
754
924
  if (
755
925
  registrationValidationSource ===
756
- PasswordlessLoginValidateWhatSources.EMAIL
926
+ PasswordlessRegistrationValidateWhatSources.EMAIL
757
927
  ) {
758
928
  if (!user.emailVerifiedOnRegistrationAt) {
759
929
  return false;
@@ -761,7 +931,7 @@ export class AuthenticationService {
761
931
  }
762
932
  if (
763
933
  registrationValidationSource ===
764
- PasswordlessLoginValidateWhatSources.MOBILE
934
+ PasswordlessRegistrationValidateWhatSources.MOBILE
765
935
  ) {
766
936
  if (!user.mobileVerifiedOnRegistrationAt) {
767
937
  return false;
@@ -876,7 +1046,7 @@ export class AuthenticationService {
876
1046
  const dummyOtp = this.getDummyOtpForUser(user);
877
1047
  if (!dummyOtp) {
878
1048
  await this.assignLoginOtp(user, type);
879
- this.notifyUserOnOtpInititateLogin(user, type);
1049
+ await this.notifyUserOnOtpInititateLogin(user, type);
880
1050
  }
881
1051
  return this.buildLoginOtpResponse(user, type);
882
1052
  }
@@ -1001,21 +1171,88 @@ export class AuthenticationService {
1001
1171
  );
1002
1172
  }
1003
1173
  if (loginType === PasswordlessLoginValidateWhatSources.MOBILE) {
1004
- const smsService = this.smsFactory.getSmsService();
1005
- smsService.sendSMSUsingTemplate(
1006
- user.mobile,
1007
- "otp-on-login",
1008
- {
1009
- solidAppName:
1010
- this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
1011
- otp: user.mobileVerificationTokenOnLogin,
1012
- mobileVerificationTokenOnLogin: user.mobileVerificationTokenOnLogin,
1013
- firstName: user.username,
1014
- fullName: user.fullName ? user.fullName : user.username,
1015
- companyLogoUrl: companyLogo,
1016
- },
1017
- this.settingService.getConfigValue<SolidCoreSetting>("shouldQueueSms"),
1174
+ const templateParams = {
1175
+ solidAppName:
1176
+ this.settingService.getConfigValue<SolidCoreSetting>("appTitle"),
1177
+ otp: user.mobileVerificationTokenOnLogin,
1178
+ mobileVerificationTokenOnLogin: user.mobileVerificationTokenOnLogin,
1179
+ firstName: user.username,
1180
+ fullName: user.fullName ? user.fullName : user.username,
1181
+ companyLogoUrl: companyLogo,
1182
+ };
1183
+
1184
+ const whatsappDestination = this.normalizeWhatsAppDestination(user.mobile);
1185
+ const gupshupTemplateAppName =
1186
+ process.env.COMMON_GUPSHUP_APP_NAME || "Gupshup";
1187
+ const whatsappTemplateId =
1188
+ this.settingService.getConfigValue<SolidCoreSetting>(
1189
+ "otpWhatsappTemplateId",
1190
+ ) || "common_otp";
1191
+ const whatsappIndependentEnabled =
1192
+ this.settingService.getConfigValue<SolidCoreSetting>(
1193
+ "otpWhatsappIndependentEnabled",
1194
+ ) !== false;
1195
+
1196
+ let smsSent = false;
1197
+ let whatsappSent = false;
1198
+ let smsErrorMessage: string | undefined;
1199
+ let whatsappErrorMessage: string | undefined;
1200
+
1201
+ this.logger.debug(
1202
+ `OTP LOGIN SMS send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}`,
1018
1203
  );
1204
+
1205
+ try {
1206
+ const smsService = this.smsFactory.getSmsService();
1207
+ await smsService.sendSMSUsingTemplate(
1208
+ user.mobile,
1209
+ "otp-on-login",
1210
+ templateParams,
1211
+ false,
1212
+ );
1213
+ smsSent = true;
1214
+ } catch (smsError: any) {
1215
+ smsErrorMessage = smsError?.message;
1216
+ this.logger.warn(
1217
+ `OTP LOGIN SMS failed: destination=${user.mobile}, message=${smsErrorMessage}`,
1218
+ );
1219
+ }
1220
+
1221
+ if (whatsappIndependentEnabled) {
1222
+ if (!whatsappDestination) {
1223
+ whatsappErrorMessage = "Normalized WhatsApp destination is empty";
1224
+ this.logger.error(
1225
+ `OTP LOGIN WhatsApp skipped: destination=${user.mobile}, message=${whatsappErrorMessage}`,
1226
+ );
1227
+ } else {
1228
+ this.logger.debug(
1229
+ `OTP LOGIN WhatsApp send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`,
1230
+ );
1231
+ try {
1232
+ await this.sendOtpToWhatsappProvider(
1233
+ whatsappDestination,
1234
+ String(whatsappTemplateId),
1235
+ String(gupshupTemplateAppName),
1236
+ String(templateParams.otp || ""),
1237
+ );
1238
+ whatsappSent = true;
1239
+ this.logger.log(
1240
+ `OTP LOGIN WhatsApp success: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`,
1241
+ );
1242
+ } catch (waError: any) {
1243
+ whatsappErrorMessage = waError?.message;
1244
+ this.logger.error(
1245
+ `OTP LOGIN WhatsApp failed: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}, message=${whatsappErrorMessage}`,
1246
+ );
1247
+ }
1248
+ }
1249
+ }
1250
+
1251
+ if (!smsSent && !whatsappSent) {
1252
+ throw new Error(
1253
+ `OTP LOGIN delivery failed on both channels. smsError=${smsErrorMessage || "unknown"}, whatsappError=${whatsappErrorMessage || "disabled/unknown"}`,
1254
+ );
1255
+ }
1019
1256
  }
1020
1257
  }
1021
1258
 
@@ -1294,7 +1531,7 @@ export class AuthenticationService {
1294
1531
  // Assuming all users do not have mobile as mandatory.
1295
1532
  if (
1296
1533
  forgotPasswordSendVerificationTokenOn ==
1297
- ForgotPasswordSendVerificationTokenOn.MOBILE &&
1534
+ ForgotPasswordSendVerificationTokenOn.MOBILE &&
1298
1535
  user.mobile
1299
1536
  ) {
1300
1537
  const smsService = this.smsFactory.getSmsService();
@@ -1323,12 +1560,10 @@ export class AuthenticationService {
1323
1560
  const user = await this.resolveUserByVerificationToken(
1324
1561
  confirmForgotPasswordDto.verificationToken,
1325
1562
  );
1326
- if (!user)
1327
- throw new UnauthorizedException("Invalid verification token");
1563
+ if (!user) throw new UnauthorizedException("Invalid verification token");
1328
1564
  if (user.lastLoginProvider !== "local")
1329
1565
  throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS);
1330
- if (!user.active)
1331
- throw new UnauthorizedException("User is inactive");
1566
+ if (!user.active) throw new UnauthorizedException("User is inactive");
1332
1567
 
1333
1568
  // 1) Atomically consume the token (only one request can succeed)
1334
1569
  const { affected } = await m
@@ -1420,7 +1655,7 @@ export class AuthenticationService {
1420
1655
  // Assuming all users do not have mobile as mandatory.
1421
1656
  if (
1422
1657
  forgotPasswordSendVerificationTokenOn ==
1423
- ForgotPasswordSendVerificationTokenOn.MOBILE &&
1658
+ ForgotPasswordSendVerificationTokenOn.MOBILE &&
1424
1659
  user.mobile
1425
1660
  ) {
1426
1661
  const smsService = this.smsFactory.getSmsService();
@@ -1,18 +1,20 @@
1
1
  import { LocalDateTimeTransformer, serializeDate } from 'src/transformers/typeorm/local-date-time-transformer';
2
- import { BadRequestException, forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common';
2
+ import { BadRequestException, ForbiddenException, forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common';
3
3
  import { ModuleRef } from "@nestjs/core";
4
4
  import { InjectEntityManager } from '@nestjs/typeorm';
5
- import { Brackets, EntityManager, EntityMetadata } from 'typeorm';
5
+ import { Brackets, EntityManager, EntityMetadata, In } from 'typeorm';
6
6
 
7
7
  import { classify } from '@angular-devkit/core/src/utils/strings';
8
8
  import { CHATTER_MESSAGE_STATUS, CHATTER_MESSAGE_SUBTYPE, CHATTER_MESSAGE_TYPE } from 'src/constants/chatter-message.constants';
9
9
  import { ERROR_MESSAGES } from 'src/constants/error-messages';
10
10
  import { PostChatterMessageDto } from 'src/dtos/post-chatter-message.dto';
11
+ import { UpdateChatterNoteMessageDto } from 'src/dtos/update-chatter-note-message.dto';
11
12
  import { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service';
12
13
  import { lowerFirst } from 'src/helpers/string.helper';
13
14
  import { ChatterMessageDetailsRepository } from 'src/repository/chatter-message-details.repository';
14
15
  import { ChatterMessageRepository } from 'src/repository/chatter-message.repository';
15
16
  import { FieldMetadataRepository } from 'src/repository/field-metadata.repository';
17
+ import { MediaRepository } from 'src/repository/media.repository';
16
18
  import { ModelMetadataRepository } from 'src/repository/model-metadata.repository';
17
19
  import { CRUDService } from 'src/services/crud.service';
18
20
  import { MediaStorageProviderType } from '../dtos/create-media-storage-provider-metadata.dto';
@@ -33,6 +35,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
33
35
  readonly repo: ChatterMessageRepository,
34
36
  // @InjectRepository(ChatterMessageDetailsRepository, 'default')
35
37
  readonly chatterMessageDetailsRepo: ChatterMessageDetailsRepository,
38
+ readonly mediaRepository: MediaRepository,
36
39
  // @InjectRepository(FieldMetadata, 'default')
37
40
  // readonly fieldMetadataRepo: Repository<FieldMetadata>,
38
41
  readonly fieldMetadataRepo: FieldMetadataRepository,
@@ -67,6 +70,21 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
67
70
  chatterMessage.updatedBy = resolvedUserId;
68
71
  }
69
72
 
73
+ private isEditableCustomNoteMessage(message: ChatterMessage): boolean {
74
+ if (message.messageType !== CHATTER_MESSAGE_TYPE.CUSTOM) {
75
+ return false;
76
+ }
77
+ return [CHATTER_MESSAGE_SUBTYPE.CUSTOM, CHATTER_MESSAGE_SUBTYPE.NOTE].includes(message.messageSubType as any);
78
+ }
79
+
80
+ private parseAttachmentIds(value?: string): number[] {
81
+ if (!value || typeof value !== 'string') return [];
82
+ return value
83
+ .split(',')
84
+ .map(v => Number(v.trim()))
85
+ .filter(v => Number.isInteger(v) && v > 0);
86
+ }
87
+
70
88
  async markCompleted(id: number) {
71
89
  const activeUser = this.requestContextService.getActiveUser();
72
90
  if (!activeUser) {
@@ -82,6 +100,98 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
82
100
  return this.repo.save(message);
83
101
  }
84
102
 
103
+ async updateCustomNoteMessage(id: number, updateDto: UpdateChatterNoteMessageDto, files: Express.Multer.File[] = []) {
104
+ const activeUser = this.requestContextService.getActiveUser();
105
+ if (!activeUser) {
106
+ throw new ForbiddenException(ERROR_MESSAGES.FORBIDDEN);
107
+ }
108
+
109
+ const message = await this.repo.findOne({ where: { id }, relations: { user: true } });
110
+ if (!message) {
111
+ throw new NotFoundException(`Entity [solid-core.chatterMessage] with id ${id} not found`);
112
+ }
113
+
114
+ if (!this.isEditableCustomNoteMessage(message)) {
115
+ throw new BadRequestException('Only custom note messages can be edited.');
116
+ }
117
+
118
+ if (!message.user?.id || message.user.id !== activeUser.sub) {
119
+ throw new ForbiddenException('You can only edit your own custom note messages.');
120
+ }
121
+
122
+ const removeAttachmentIds = this.parseAttachmentIds(updateDto?.removeAttachmentIds);
123
+ const hasMessageBody = typeof updateDto?.messageBody === 'string';
124
+ const trimmedMessageBody = (updateDto?.messageBody ?? '').trim();
125
+ const hasNewFiles = Array.isArray(files) && files.length > 0;
126
+
127
+ if (!hasMessageBody && removeAttachmentIds.length === 0 && !hasNewFiles) {
128
+ throw new BadRequestException('No note changes submitted.');
129
+ }
130
+
131
+ if (hasMessageBody && trimmedMessageBody.length === 0) {
132
+ throw new BadRequestException('Message body cannot be empty.');
133
+ }
134
+
135
+ if (hasMessageBody) {
136
+ message.messageBody = trimmedMessageBody;
137
+ }
138
+ message.updatedBy = activeUser.sub;
139
+ // Ensure updatedAt changes even for attachment-only edits.
140
+ message.updatedAt = new Date();
141
+ const savedMessage = await this.repo.save(message);
142
+
143
+ if (removeAttachmentIds.length > 0 || hasNewFiles) {
144
+ const model = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
145
+ fields: {
146
+ model: true,
147
+ mediaStorageProvider: true,
148
+ },
149
+ module: true,
150
+ });
151
+
152
+ const mediaFields = model.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
153
+ const attachmentFieldIds = mediaFields.map(field => field.id);
154
+
155
+ if (removeAttachmentIds.length > 0 && attachmentFieldIds.length > 0) {
156
+ const mediaToRemove = await this.mediaRepository.find({
157
+ where: {
158
+ id: In(removeAttachmentIds),
159
+ entityId: savedMessage.id,
160
+ modelMetadata: { id: model.id },
161
+ fieldMetadata: { id: In(attachmentFieldIds) },
162
+ },
163
+ relations: {
164
+ mediaStorageProviderMetadata: true,
165
+ fieldMetadata: true,
166
+ },
167
+ });
168
+
169
+ for (const media of mediaToRemove) {
170
+ const storageType = media.mediaStorageProviderMetadata?.type as MediaStorageProviderType;
171
+ const storageProvider = await getMediaStorageProvider(this.moduleRef, storageType);
172
+ await storageProvider.deleteByMediaRecord(media);
173
+ }
174
+
175
+ if (mediaToRemove.length > 0) {
176
+ await this.mediaRepository.remove(mediaToRemove);
177
+ }
178
+ }
179
+
180
+ for (const mediaField of mediaFields) {
181
+ const storageProviderMetadata = mediaField.mediaStorageProvider;
182
+ const storageProviderType = storageProviderMetadata.type as MediaStorageProviderType;
183
+ const storageProvider = await getMediaStorageProvider(this.moduleRef, storageProviderType);
184
+
185
+ const media = files.filter(multerFile => multerFile.fieldname === mediaField.name);
186
+ if (media.length > 0) {
187
+ await storageProvider.store(media, savedMessage, mediaField);
188
+ }
189
+ }
190
+ }
191
+
192
+ return savedMessage;
193
+ }
194
+
85
195
  async postMessage(postDto: PostChatterMessageDto, files: Express.Multer.File[] = []) {
86
196
  const chatterMessage = new ChatterMessage();
87
197
  chatterMessage.messageType = CHATTER_MESSAGE_TYPE.CUSTOM;
@@ -120,6 +120,18 @@ export class FileS3StorageProvider<T> implements MediaStorageProvider<T> {
120
120
  }
121
121
  }
122
122
 
123
+ async deleteByMediaRecord(media: Media): Promise<void> {
124
+ const storageProvider = media?.mediaStorageProviderMetadata;
125
+ if (!storageProvider) {
126
+ throw new Error(`mediaStorageProviderMetadata is not populated for media id ${media?.id ?? 'unknown'}`);
127
+ }
128
+ if (!storageProvider?.bucketName || !media?.relativeUri) {
129
+ return;
130
+ }
131
+ const region = this.getEffectiveRegion(storageProvider.region);
132
+ await this.s3FileService.delete(`${storageProvider.bucketName}:${media.relativeUri}`, { region });
133
+ }
134
+
123
135
  /**
124
136
  * Get the effective region to use for S3 operations.
125
137
  * Uses the provider-specific region if configured, otherwise falls back to env variable.
@@ -110,6 +110,13 @@ export class FileStorageProvider<T> implements MediaStorageProvider<T> {
110
110
  // });
111
111
  }
112
112
 
113
+ async deleteByMediaRecord(media: Media): Promise<void> {
114
+ if (!media?.relativeUri) {
115
+ return;
116
+ }
117
+ await this.fileService.delete(this.getFullFilePath(media.relativeUri));
118
+ }
119
+
113
120
  private getFullFilePath(fileName: string): string {
114
121
  const base = this.settingService.getConfigValue<SolidCoreSetting>("fileStorageDir")
115
122
  || DEFAULT_MEDIA_FILE_STORAGE_DIR;
@@ -122,4 +129,4 @@ export class FileStorageProvider<T> implements MediaStorageProvider<T> {
122
129
  private getFileName(file: Express.Multer.File): string {
123
130
  return `${file.filename}-${file.originalname}`;
124
131
  }
125
- }
132
+ }