@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.
- package/dist/commands/test-data.command.d.ts +2 -0
- package/dist/commands/test-data.command.d.ts.map +1 -1
- package/dist/commands/test-data.command.js +32 -6
- package/dist/commands/test-data.command.js.map +1 -1
- package/dist/controllers/chatter-message.controller.d.ts +2 -0
- package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
- package/dist/controllers/chatter-message.controller.js +17 -0
- package/dist/controllers/chatter-message.controller.js.map +1 -1
- package/dist/controllers/gupshup-webhook.controller.d.ts +11 -0
- package/dist/controllers/gupshup-webhook.controller.d.ts.map +1 -0
- package/dist/controllers/gupshup-webhook.controller.js +87 -0
- package/dist/controllers/gupshup-webhook.controller.js.map +1 -0
- package/dist/controllers/meta-cloud-whatsapp-webhook.controller.d.ts +16 -0
- package/dist/controllers/meta-cloud-whatsapp-webhook.controller.d.ts.map +1 -0
- package/dist/controllers/meta-cloud-whatsapp-webhook.controller.js +133 -0
- package/dist/controllers/meta-cloud-whatsapp-webhook.controller.js.map +1 -0
- package/dist/dtos/update-chatter-note-message.dto.d.ts +5 -0
- package/dist/dtos/update-chatter-note-message.dto.d.ts.map +1 -0
- package/dist/dtos/update-chatter-note-message.dto.js +34 -0
- package/dist/dtos/update-chatter-note-message.dto.js.map +1 -0
- package/dist/factories/whatsapp.factory.d.ts.map +1 -1
- package/dist/factories/whatsapp.factory.js +13 -11
- package/dist/factories/whatsapp.factory.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +1 -0
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/listeners/user-registration.listener.d.ts +5 -2
- package/dist/listeners/user-registration.listener.d.ts.map +1 -1
- package/dist/listeners/user-registration.listener.js +37 -4
- package/dist/listeners/user-registration.listener.js.map +1 -1
- package/dist/seeders/module-test-data.service.d.ts +2 -0
- package/dist/seeders/module-test-data.service.d.ts.map +1 -1
- package/dist/seeders/module-test-data.service.js +61 -0
- package/dist/seeders/module-test-data.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +1 -0
- package/dist/services/authentication.service.d.ts +6 -1
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +144 -14
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/chatter-message.service.d.ts +7 -1
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +94 -2
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts +1 -0
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +11 -0
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
- package/dist/services/mediaStorageProviders/file-storage-provider.d.ts +1 -0
- package/dist/services/mediaStorageProviders/file-storage-provider.d.ts.map +1 -1
- package/dist/services/mediaStorageProviders/file-storage-provider.js +6 -0
- package/dist/services/mediaStorageProviders/file-storage-provider.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +174 -0
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +98 -0
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/services/user.service.js +1 -1
- package/dist/services/user.service.js.map +1 -1
- package/dist/services/whatsapp/GupshupOtpWhatsappService.d.ts +11 -0
- package/dist/services/whatsapp/GupshupOtpWhatsappService.d.ts.map +1 -0
- package/dist/services/whatsapp/GupshupOtpWhatsappService.js +127 -0
- package/dist/services/whatsapp/GupshupOtpWhatsappService.js.map +1 -0
- package/dist/services/whatsapp/MetaCloudWhatsappService.d.ts +17 -0
- package/dist/services/whatsapp/MetaCloudWhatsappService.d.ts.map +1 -0
- package/dist/services/whatsapp/MetaCloudWhatsappService.js +209 -0
- package/dist/services/whatsapp/MetaCloudWhatsappService.js.map +1 -0
- package/dist/solid-core.module.d.ts +2 -2
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +14 -6
- package/dist/solid-core.module.js.map +1 -1
- package/dist-tests/api/authenticate.spec.js +119 -0
- package/dist-tests/api/authenticate.spec.js.map +1 -0
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +97 -0
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +1 -0
- package/dist-tests/api/ping.spec.js +21 -0
- package/dist-tests/api/ping.spec.js.map +1 -0
- package/dist-tests/helpers/auth.js +41 -0
- package/dist-tests/helpers/auth.js.map +1 -0
- package/dist-tests/helpers/env.js +11 -0
- package/dist-tests/helpers/env.js.map +1 -0
- package/docs/grouping-enhancements.md +89 -0
- package/docs/java-spring/README.md +3 -0
- package/docs/java-spring/solid-core-module-deep-dive-report.md +1317 -0
- package/docs/seed-changes.md +65 -0
- package/docs/test-data-workflow.md +200 -0
- package/docs/type-declaration-import-issue.md +24 -0
- package/package.json +1 -1
- package/src/commands/test-data.command.ts +29 -6
- package/src/controllers/chatter-message.controller.ts +12 -0
- package/src/controllers/gupshup-webhook.controller.ts +91 -0
- package/src/controllers/meta-cloud-whatsapp-webhook.controller.ts +155 -0
- package/src/dtos/update-chatter-note-message.dto.ts +14 -0
- package/src/factories/whatsapp.factory.ts +33 -32
- package/src/helpers/field-crud-managers/field-quality-check-fixes.md +178 -0
- package/src/index.ts +2 -0
- package/src/interfaces.ts +1 -1
- package/src/listeners/user-registration.listener.ts +49 -6
- package/src/seeders/module-test-data.service.ts +79 -2
- package/src/seeders/seed-data/solid-core-metadata.json +1 -0
- package/src/services/authentication.service.ts +281 -46
- package/src/services/chatter-message.service.ts +112 -2
- package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +12 -0
- package/src/services/mediaStorageProviders/file-storage-provider.ts +8 -1
- package/src/services/settings/default-settings-provider.service.ts +98 -0
- package/src/services/user.service.ts +1 -1
- package/src/services/whatsapp/GupshupOtpWhatsappService.ts +174 -0
- package/src/services/whatsapp/MetaCloudWhatsappService.ts +253 -0
- package/src/solid-core.module.ts +205 -196
- package/src/testing/README.md +15 -0
- package/.claude/settings.local.json +0 -15
- package/CLAUDE.md +0 -18
- package/CURRENT_PROMPT +0 -6
- 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>(
|
|
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 =
|
|
199
|
-
|
|
200
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
689
|
+
PasswordlessRegistrationValidateWhatSources.MOBILE
|
|
636
690
|
) {
|
|
637
|
-
const
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|