customer-registration 0.0.23 → 0.0.47

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 (32) hide show
  1. package/.medusa/server/src/api/auth/customer/emailpass/route.js +58 -0
  2. package/.medusa/server/src/api/store/customers/email/otp/resend/route.js +78 -0
  3. package/.medusa/server/src/api/store/customers/email/otp/verify/route.js +54 -40
  4. package/.medusa/server/src/api/store/customers/forget-password/otp/resend/route.js +52 -0
  5. package/.medusa/server/src/api/store/customers/forget-password/otp/verify/route.js +42 -0
  6. package/.medusa/server/src/api/store/customers/phone/otp/resend/route.js +81 -0
  7. package/.medusa/server/src/api/store/customers/phone/otp/verify/route.js +60 -41
  8. package/.medusa/server/src/errors/otp-errors.js +29 -0
  9. package/.medusa/server/src/loaders/index.js +29 -9
  10. package/.medusa/server/src/modules/customer-registration/index.js +43 -9
  11. package/.medusa/server/src/modules/customer-registration/migrations/Migration20251122112915AddEmailPhoneVerifiedColumns.js +67 -0
  12. package/.medusa/server/src/modules/customer-registration/migrations/Migration20251122112916CreateCustomerOtpTable.js +56 -0
  13. package/.medusa/server/src/modules/customer-registration/models/customer-otp.js +65 -32
  14. package/.medusa/server/src/modules/customer-registration/services/otp-service.js +226 -0
  15. package/.medusa/server/src/services/notification-service.js +81 -0
  16. package/.medusa/server/src/subscribers/customer-created.js +42 -0
  17. package/.medusa/server/src/types/plugin-options.js +30 -0
  18. package/.medusa/server/src/utils/crypto.js +52 -0
  19. package/.medusa/server/src/utils/customer-update.js +48 -0
  20. package/.medusa/server/src/utils/otp-generator.js +27 -0
  21. package/.medusa/server/src/utils/token-generator.js +11 -0
  22. package/README.md +156 -32
  23. package/package.json +3 -1
  24. package/.medusa/server/src/api/store/customers/phone/otp/send/route.js +0 -48
  25. package/.medusa/server/src/api/store/customers/route.js +0 -77
  26. package/.medusa/server/src/modules/customer-registration/__tests__/config.spec.js +0 -61
  27. package/.medusa/server/src/modules/customer-registration/config.js +0 -73
  28. package/.medusa/server/src/modules/customer-registration/constants.js +0 -5
  29. package/.medusa/server/src/modules/customer-registration/migrations/Migration20250118000000AddEmailVerifiedColumn.js +0 -21
  30. package/.medusa/server/src/modules/customer-registration/migrations/Migration20250118001000CreateCustomerOtpTable.js +0 -48
  31. package/.medusa/server/src/modules/customer-registration/service.js +0 -242
  32. package/.medusa/server/src/modules/customer-registration/types.js +0 -3
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendOTP = sendOTP;
4
+ /**
5
+ * Send OTP via Medusa Notification module
6
+ */
7
+ async function sendOTP(notificationService, params) {
8
+ const { otp, channelType, address, otpType, customerName, config } = params;
9
+ let channel;
10
+ let template;
11
+ let subject;
12
+ let resendThrottleSeconds;
13
+ switch (otpType) {
14
+ case "email_verification":
15
+ channel = config.email.channel;
16
+ template = config.email.template;
17
+ subject = config.email.subject;
18
+ break;
19
+ case "phone_verification":
20
+ channel = config.phone.channel;
21
+ template = config.phone.template;
22
+ break;
23
+ case "forget_password":
24
+ channel = config.forgetPassword.channel;
25
+ template = config.forgetPassword.template;
26
+ subject = config.forgetPassword.subject;
27
+ break;
28
+ default:
29
+ channel = "email";
30
+ }
31
+ const expiryMinutes = config.otpExpiryMinutes;
32
+ // Prepare notification data
33
+ const notificationData = {
34
+ channel,
35
+ to: address,
36
+ data: {
37
+ otp,
38
+ expiry: expiryMinutes,
39
+ expiry_minutes: expiryMinutes,
40
+ customer_name: customerName || "Customer",
41
+ },
42
+ };
43
+ if (template) {
44
+ notificationData.template = template;
45
+ }
46
+ if (subject && channelType === "email") {
47
+ notificationData.subject = subject;
48
+ }
49
+ try {
50
+ // Try to send via notification service
51
+ // Medusa v2 notification service typically uses 'send' method
52
+ const service = notificationService;
53
+ if (typeof service.send === "function") {
54
+ await service.send(notificationData);
55
+ }
56
+ else if (typeof service.create === "function") {
57
+ await service.create(notificationData);
58
+ }
59
+ else if (typeof service.createNotification === "function") {
60
+ await service.createNotification(notificationData);
61
+ }
62
+ else {
63
+ // If no method found, log and continue (fallback will handle it)
64
+ throw new Error("Notification service method not available. Available methods: " + Object.getOwnPropertyNames(service).join(", "));
65
+ }
66
+ }
67
+ catch (error) {
68
+ // Fallback to console log if notification module not configured
69
+ // This is expected in development/testing environments
70
+ const errorMessage = error instanceof Error ? error.message : String(error);
71
+ console.log(`[customer-registration] OTP dispatch fallback (notification service unavailable):`);
72
+ console.log(` Channel: ${channelType}`);
73
+ console.log(` To: ${address}`);
74
+ console.log(` OTP: ${otp}`);
75
+ console.log(` Expires in: ${expiryMinutes} minutes`);
76
+ console.log(` Error: ${errorMessage}`);
77
+ // In production, you might want to throw here or use a logging service
78
+ // For now, we silently continue to allow development without notification setup
79
+ }
80
+ }
81
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibm90aWZpY2F0aW9uLXNlcnZpY2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvc2VydmljZXMvbm90aWZpY2F0aW9uLXNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFlQSwwQkFpRkM7QUFwRkQ7O0dBRUc7QUFDSSxLQUFLLFVBQVUsT0FBTyxDQUMzQixtQkFBK0MsRUFDL0MsTUFBcUI7SUFFckIsTUFBTSxFQUFFLEdBQUcsRUFBRSxXQUFXLEVBQUUsT0FBTyxFQUFFLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxFQUFFLEdBQUcsTUFBTSxDQUFBO0lBRTNFLElBQUksT0FBZSxDQUFBO0lBQ25CLElBQUksUUFBbUMsQ0FBQTtJQUN2QyxJQUFJLE9BQTJCLENBQUE7SUFDL0IsSUFBSSxxQkFBNkIsQ0FBQTtJQUVqQyxRQUFRLE9BQU8sRUFBRSxDQUFDO1FBQ2hCLEtBQUssb0JBQW9CO1lBQ3ZCLE9BQU8sR0FBRyxNQUFNLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQTtZQUM5QixRQUFRLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUE7WUFDaEMsT0FBTyxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFBO1lBQzlCLE1BQUs7UUFDUCxLQUFLLG9CQUFvQjtZQUN2QixPQUFPLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUE7WUFDOUIsUUFBUSxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsUUFBUSxDQUFBO1lBQ2hDLE1BQUs7UUFDUCxLQUFLLGlCQUFpQjtZQUNwQixPQUFPLEdBQUcsTUFBTSxDQUFDLGNBQWMsQ0FBQyxPQUFPLENBQUE7WUFDdkMsUUFBUSxHQUFHLE1BQU0sQ0FBQyxjQUFjLENBQUMsUUFBUSxDQUFBO1lBQ3pDLE9BQU8sR0FBRyxNQUFNLENBQUMsY0FBYyxDQUFDLE9BQU8sQ0FBQTtZQUN2QyxNQUFLO1FBQ1A7WUFDRSxPQUFPLEdBQUcsT0FBTyxDQUFBO0lBQ3JCLENBQUM7SUFFRCxNQUFNLGFBQWEsR0FBRyxNQUFNLENBQUMsZ0JBQWdCLENBQUE7SUFFN0MsNEJBQTRCO0lBQzVCLE1BQU0sZ0JBQWdCLEdBQVE7UUFDNUIsT0FBTztRQUNQLEVBQUUsRUFBRSxPQUFPO1FBQ1gsSUFBSSxFQUFFO1lBQ0osR0FBRztZQUNILE1BQU0sRUFBRSxhQUFhO1lBQ3JCLGNBQWMsRUFBRSxhQUFhO1lBQzdCLGFBQWEsRUFBRSxZQUFZLElBQUksVUFBVTtTQUMxQztLQUNGLENBQUE7SUFFRCxJQUFJLFFBQVEsRUFBRSxDQUFDO1FBQ2IsZ0JBQWdCLENBQUMsUUFBUSxHQUFHLFFBQVEsQ0FBQTtJQUN0QyxDQUFDO0lBRUQsSUFBSSxPQUFPLElBQUksV0FBVyxLQUFLLE9BQU8sRUFBRSxDQUFDO1FBQ3ZDLGdCQUFnQixDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUE7SUFDcEMsQ0FBQztJQUVELElBQUksQ0FBQztRQUNILHVDQUF1QztRQUN2Qyw4REFBOEQ7UUFDOUQsTUFBTSxPQUFPLEdBQUcsbUJBQTBCLENBQUE7UUFFMUMsSUFBSSxPQUFPLE9BQU8sQ0FBQyxJQUFJLEtBQUssVUFBVSxFQUFFLENBQUM7WUFDdkMsTUFBTSxPQUFPLENBQUMsSUFBSSxDQUFDLGdCQUFnQixDQUFDLENBQUE7UUFDdEMsQ0FBQzthQUFNLElBQUksT0FBTyxPQUFPLENBQUMsTUFBTSxLQUFLLFVBQVUsRUFBRSxDQUFDO1lBQ2hELE1BQU0sT0FBTyxDQUFDLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFBO1FBQ3hDLENBQUM7YUFBTSxJQUFJLE9BQU8sT0FBTyxDQUFDLGtCQUFrQixLQUFLLFVBQVUsRUFBRSxDQUFDO1lBQzVELE1BQU0sT0FBTyxDQUFDLGtCQUFrQixDQUFDLGdCQUFnQixDQUFDLENBQUE7UUFDcEQsQ0FBQzthQUFNLENBQUM7WUFDTixpRUFBaUU7WUFDakUsTUFBTSxJQUFJLEtBQUssQ0FBQyxnRUFBZ0UsR0FBRyxNQUFNLENBQUMsbUJBQW1CLENBQUMsT0FBTyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUE7UUFDcEksQ0FBQztJQUNILENBQUM7SUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1FBQ2YsZ0VBQWdFO1FBQ2hFLHVEQUF1RDtRQUN2RCxNQUFNLFlBQVksR0FBRyxLQUFLLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUE7UUFDM0UsT0FBTyxDQUFDLEdBQUcsQ0FBQyxtRkFBbUYsQ0FBQyxDQUFBO1FBQ2hHLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxXQUFXLEVBQUUsQ0FBQyxDQUFBO1FBQ3hDLE9BQU8sQ0FBQyxHQUFHLENBQUMsU0FBUyxPQUFPLEVBQUUsQ0FBQyxDQUFBO1FBQy9CLE9BQU8sQ0FBQyxHQUFHLENBQUMsVUFBVSxHQUFHLEVBQUUsQ0FBQyxDQUFBO1FBQzVCLE9BQU8sQ0FBQyxHQUFHLENBQUMsaUJBQWlCLGFBQWEsVUFBVSxDQUFDLENBQUE7UUFDckQsT0FBTyxDQUFDLEdBQUcsQ0FBQyxZQUFZLFlBQVksRUFBRSxDQUFDLENBQUE7UUFFdkMsdUVBQXVFO1FBQ3ZFLGdGQUFnRjtJQUNsRixDQUFDO0FBQ0gsQ0FBQyJ9
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ exports.default = customerCreatedHandler;
5
+ const utils_1 = require("@medusajs/framework/utils");
6
+ const notification_service_1 = require("../services/notification-service");
7
+ const index_1 = require("../modules/customer-registration/index");
8
+ async function customerCreatedHandler({ event: { data }, container, }) {
9
+ const config = container.resolve("pluginOptions");
10
+ // Check if auto-send is enabled
11
+ if (!config.email.autoSendOnRegistration) {
12
+ return;
13
+ }
14
+ const customerService = container.resolve(utils_1.Modules.CUSTOMER);
15
+ const notificationService = container.resolve(utils_1.Modules.NOTIFICATION);
16
+ const otpService = container.resolve(index_1.CUSTOMER_REGISTRATION_MODULE);
17
+ // Get customer details
18
+ const customer = await customerService.retrieveCustomer(data.id);
19
+ if (!customer.email) {
20
+ return;
21
+ }
22
+ try {
23
+ // Create and send email OTP
24
+ const { otp, token } = await otpService.createOTP(customer.id, "email", customer.email, "email_verification", config);
25
+ // Send notification
26
+ await (0, notification_service_1.sendOTP)(notificationService, {
27
+ otp,
28
+ channelType: "email",
29
+ address: customer.email,
30
+ otpType: "email_verification",
31
+ customerName: `${customer.first_name || ""} ${customer.last_name || ""}`.trim() || undefined,
32
+ config,
33
+ });
34
+ }
35
+ catch (error) {
36
+ console.error("[customer-registration] Error sending email OTP on registration:", error);
37
+ }
38
+ }
39
+ exports.config = {
40
+ event: "customer.created",
41
+ };
42
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3VzdG9tZXItY3JlYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9zdWJzY3JpYmVycy9jdXN0b21lci1jcmVhdGVkLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQVFBLHlDQW9EQztBQTNERCxxREFBbUQ7QUFHbkQsMkVBQTBEO0FBRTFELGtFQUFxRjtBQUV0RSxLQUFLLFVBQVUsc0JBQXNCLENBQUMsRUFDbkQsS0FBSyxFQUFFLEVBQUUsSUFBSSxFQUFFLEVBQ2YsU0FBUyxHQUNzRDtJQUMvRCxNQUFNLE1BQU0sR0FBRyxTQUFTLENBQUMsT0FBTyxDQUM5QixlQUFlLENBQ1csQ0FBQTtJQUU1QixnQ0FBZ0M7SUFDaEMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsc0JBQXNCLEVBQUUsQ0FBQztRQUN6QyxPQUFNO0lBQ1IsQ0FBQztJQUVELE1BQU0sZUFBZSxHQUFHLFNBQVMsQ0FBQyxPQUFPLENBQ3ZDLGVBQU8sQ0FBQyxRQUFRLENBQ1MsQ0FBQTtJQUMzQixNQUFNLG1CQUFtQixHQUFHLFNBQVMsQ0FBQyxPQUFPLENBQzNDLGVBQU8sQ0FBQyxZQUFZLENBQ1MsQ0FBQTtJQUMvQixNQUFNLFVBQVUsR0FBRyxTQUFTLENBQUMsT0FBTyxDQUNsQyxvQ0FBNEIsQ0FDZixDQUFBO0lBRWYsdUJBQXVCO0lBQ3ZCLE1BQU0sUUFBUSxHQUFHLE1BQU0sZUFBZSxDQUFDLGdCQUFnQixDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsQ0FBQTtJQUVoRSxJQUFJLENBQUMsUUFBUSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ3BCLE9BQU07SUFDUixDQUFDO0lBRUQsSUFBSSxDQUFDO1FBQ0gsNEJBQTRCO1FBQzVCLE1BQU0sRUFBRSxHQUFHLEVBQUUsS0FBSyxFQUFFLEdBQUcsTUFBTSxVQUFVLENBQUMsU0FBUyxDQUMvQyxRQUFRLENBQUMsRUFBRSxFQUNYLE9BQU8sRUFDUCxRQUFRLENBQUMsS0FBSyxFQUNkLG9CQUFvQixFQUNwQixNQUFNLENBQ1AsQ0FBQTtRQUVELG9CQUFvQjtRQUNwQixNQUFNLElBQUEsOEJBQU8sRUFBQyxtQkFBbUIsRUFBRTtZQUNqQyxHQUFHO1lBQ0gsV0FBVyxFQUFFLE9BQU87WUFDcEIsT0FBTyxFQUFFLFFBQVEsQ0FBQyxLQUFLO1lBQ3ZCLE9BQU8sRUFBRSxvQkFBb0I7WUFDN0IsWUFBWSxFQUFFLEdBQUcsUUFBUSxDQUFDLFVBQVUsSUFBSSxFQUFFLElBQUksUUFBUSxDQUFDLFNBQVMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLEVBQUUsSUFBSSxTQUFTO1lBQzVGLE1BQU07U0FDUCxDQUFDLENBQUE7SUFDSixDQUFDO0lBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztRQUNmLE9BQU8sQ0FBQyxLQUFLLENBQUMsa0VBQWtFLEVBQUUsS0FBSyxDQUFDLENBQUE7SUFDMUYsQ0FBQztBQUNILENBQUM7QUFFWSxRQUFBLE1BQU0sR0FBcUI7SUFDdEMsS0FBSyxFQUFFLGtCQUFrQjtDQUMxQixDQUFBIn0=
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizePluginOptions = normalizePluginOptions;
4
+ function normalizePluginOptions(options) {
5
+ return {
6
+ otpLength: options?.otpLength ?? 6,
7
+ otpCharset: options?.otpCharset ?? "numeric",
8
+ otpExpiryMinutes: options?.otpExpiryMinutes ?? 15,
9
+ maxAttempts: options?.maxAttempts ?? 5,
10
+ email: {
11
+ channel: options?.email?.channel ?? "email",
12
+ template: options?.email?.template ?? null,
13
+ subject: options?.email?.subject ?? "Verify your Medusa account",
14
+ resendThrottleSeconds: options?.email?.resendThrottleSeconds ?? 90,
15
+ autoSendOnRegistration: options?.email?.autoSendOnRegistration ?? true,
16
+ },
17
+ phone: {
18
+ channel: options?.phone?.channel ?? "sms",
19
+ template: options?.phone?.template ?? null,
20
+ resendThrottleSeconds: options?.phone?.resendThrottleSeconds ?? 60,
21
+ },
22
+ forgetPassword: {
23
+ channel: options?.forgetPassword?.channel ?? "email",
24
+ template: options?.forgetPassword?.template ?? null,
25
+ subject: options?.forgetPassword?.subject ?? "Reset your password",
26
+ resendThrottleSeconds: options?.forgetPassword?.resendThrottleSeconds ?? 120,
27
+ },
28
+ };
29
+ }
30
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2luLW9wdGlvbnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvdHlwZXMvcGx1Z2luLW9wdGlvbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFnQ0Esd0RBeUJDO0FBekJELFNBQWdCLHNCQUFzQixDQUFDLE9BQXVCO0lBQzVELE9BQU87UUFDTCxTQUFTLEVBQUUsT0FBTyxFQUFFLFNBQVMsSUFBSSxDQUFDO1FBQ2xDLFVBQVUsRUFBRSxPQUFPLEVBQUUsVUFBVSxJQUFJLFNBQVM7UUFDNUMsZ0JBQWdCLEVBQUUsT0FBTyxFQUFFLGdCQUFnQixJQUFJLEVBQUU7UUFDakQsV0FBVyxFQUFFLE9BQU8sRUFBRSxXQUFXLElBQUksQ0FBQztRQUN0QyxLQUFLLEVBQUU7WUFDTCxPQUFPLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxPQUFPLElBQUksT0FBTztZQUMzQyxRQUFRLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxRQUFRLElBQUksSUFBSTtZQUMxQyxPQUFPLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxPQUFPLElBQUksNEJBQTRCO1lBQ2hFLHFCQUFxQixFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUscUJBQXFCLElBQUksRUFBRTtZQUNsRSxzQkFBc0IsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLHNCQUFzQixJQUFJLElBQUk7U0FDdkU7UUFDRCxLQUFLLEVBQUU7WUFDTCxPQUFPLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxPQUFPLElBQUksS0FBSztZQUN6QyxRQUFRLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxRQUFRLElBQUksSUFBSTtZQUMxQyxxQkFBcUIsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLHFCQUFxQixJQUFJLEVBQUU7U0FDbkU7UUFDRCxjQUFjLEVBQUU7WUFDZCxPQUFPLEVBQUUsT0FBTyxFQUFFLGNBQWMsRUFBRSxPQUFPLElBQUksT0FBTztZQUNwRCxRQUFRLEVBQUUsT0FBTyxFQUFFLGNBQWMsRUFBRSxRQUFRLElBQUksSUFBSTtZQUNuRCxPQUFPLEVBQUUsT0FBTyxFQUFFLGNBQWMsRUFBRSxPQUFPLElBQUkscUJBQXFCO1lBQ2xFLHFCQUFxQixFQUFFLE9BQU8sRUFBRSxjQUFjLEVBQUUscUJBQXFCLElBQUksR0FBRztTQUM3RTtLQUNGLENBQUE7QUFDSCxDQUFDIn0=
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.hashOTP = hashOTP;
37
+ exports.compareOTP = compareOTP;
38
+ const bcrypt = __importStar(require("bcryptjs"));
39
+ /**
40
+ * Hash OTP using bcrypt
41
+ */
42
+ async function hashOTP(otp) {
43
+ const saltRounds = 10;
44
+ return await bcrypt.hash(otp, saltRounds);
45
+ }
46
+ /**
47
+ * Compare OTP with hash
48
+ */
49
+ async function compareOTP(otp, hash) {
50
+ return await bcrypt.compare(otp, hash);
51
+ }
52
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3J5cHRvLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3V0aWxzL2NyeXB0by50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUtBLDBCQUdDO0FBS0QsZ0NBRUM7QUFmRCxpREFBa0M7QUFFbEM7O0dBRUc7QUFDSSxLQUFLLFVBQVUsT0FBTyxDQUFDLEdBQVc7SUFDdkMsTUFBTSxVQUFVLEdBQUcsRUFBRSxDQUFBO0lBQ3JCLE9BQU8sTUFBTSxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRSxVQUFVLENBQUMsQ0FBQTtBQUMzQyxDQUFDO0FBRUQ7O0dBRUc7QUFDSSxLQUFLLFVBQVUsVUFBVSxDQUFDLEdBQVcsRUFBRSxJQUFZO0lBQ3hELE9BQU8sTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsQ0FBQTtBQUN4QyxDQUFDIn0=
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.updateCustomerVerificationFields = updateCustomerVerificationFields;
4
+ exports.findCustomerByPhone = findCustomerByPhone;
5
+ const utils_1 = require("@medusajs/framework/utils");
6
+ /**
7
+ * Update customer verification fields using EntityManager for direct database access
8
+ * This is necessary because custom fields may not be supported by the customer service API
9
+ */
10
+ async function updateCustomerVerificationFields(manager, customerId, fields) {
11
+ try {
12
+ const updateData = {};
13
+ if (fields.email_verified !== undefined) {
14
+ updateData.email_verified = fields.email_verified;
15
+ }
16
+ if (fields.phone_verified !== undefined) {
17
+ updateData.phone_verified = fields.phone_verified;
18
+ }
19
+ if (Object.keys(updateData).length === 0) {
20
+ return;
21
+ }
22
+ // Use native update for custom fields
23
+ await manager.nativeUpdate("customer", { id: customerId }, updateData);
24
+ }
25
+ catch (error) {
26
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, `Failed to update customer verification fields: ${error instanceof Error ? error.message : "Unknown error"}`);
27
+ }
28
+ }
29
+ /**
30
+ * Find customer by phone number using EntityManager
31
+ * Fallback when customer service doesn't support phone filter
32
+ */
33
+ async function findCustomerByPhone(manager, phone) {
34
+ try {
35
+ // Use native query for direct database access
36
+ const connection = manager.getConnection();
37
+ const result = await connection.execute('SELECT id FROM customer WHERE phone = $1 LIMIT 1', [phone]);
38
+ if (Array.isArray(result) && result.length > 0 && result[0]) {
39
+ return { id: result[0].id };
40
+ }
41
+ return null;
42
+ }
43
+ catch (error) {
44
+ console.error("[customer-registration] Error finding customer by phone:", error);
45
+ return null;
46
+ }
47
+ }
48
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3VzdG9tZXItdXBkYXRlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3V0aWxzL2N1c3RvbWVyLXVwZGF0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQVFBLDRFQWdDQztBQU1ELGtEQXFCQztBQWpFRCxxREFBdUQ7QUFFdkQ7OztHQUdHO0FBQ0ksS0FBSyxVQUFVLGdDQUFnQyxDQUNwRCxPQUFzQixFQUN0QixVQUFrQixFQUNsQixNQUE4RDtJQUU5RCxJQUFJLENBQUM7UUFDSCxNQUFNLFVBQVUsR0FBNEIsRUFBRSxDQUFBO1FBRTlDLElBQUksTUFBTSxDQUFDLGNBQWMsS0FBSyxTQUFTLEVBQUUsQ0FBQztZQUN4QyxVQUFVLENBQUMsY0FBYyxHQUFHLE1BQU0sQ0FBQyxjQUFjLENBQUE7UUFDbkQsQ0FBQztRQUVELElBQUksTUFBTSxDQUFDLGNBQWMsS0FBSyxTQUFTLEVBQUUsQ0FBQztZQUN4QyxVQUFVLENBQUMsY0FBYyxHQUFHLE1BQU0sQ0FBQyxjQUFjLENBQUE7UUFDbkQsQ0FBQztRQUVELElBQUksTUFBTSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFLENBQUM7WUFDekMsT0FBTTtRQUNSLENBQUM7UUFFRCxzQ0FBc0M7UUFDdEMsTUFBTSxPQUFPLENBQUMsWUFBWSxDQUN4QixVQUFVLEVBQ1YsRUFBRSxFQUFFLEVBQUUsVUFBVSxFQUFFLEVBQ2xCLFVBQVUsQ0FDWCxDQUFBO0lBQ0gsQ0FBQztJQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7UUFDZixNQUFNLElBQUksbUJBQVcsQ0FDbkIsbUJBQVcsQ0FBQyxLQUFLLENBQUMsZ0JBQWdCLEVBQ2xDLGtEQUFrRCxLQUFLLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxlQUFlLEVBQUUsQ0FDN0csQ0FBQTtJQUNILENBQUM7QUFDSCxDQUFDO0FBRUQ7OztHQUdHO0FBQ0ksS0FBSyxVQUFVLG1CQUFtQixDQUN2QyxPQUFzQixFQUN0QixLQUFhO0lBRWIsSUFBSSxDQUFDO1FBQ0gsOENBQThDO1FBQzlDLE1BQU0sVUFBVSxHQUFHLE9BQU8sQ0FBQyxhQUFhLEVBQUUsQ0FBQTtRQUMxQyxNQUFNLE1BQU0sR0FBRyxNQUFNLFVBQVUsQ0FBQyxPQUFPLENBQ3JDLGtEQUFrRCxFQUNsRCxDQUFDLEtBQUssQ0FBQyxDQUNSLENBQUE7UUFFRCxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLElBQUksTUFBTSxDQUFDLE1BQU0sR0FBRyxDQUFDLElBQUksTUFBTSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7WUFDNUQsT0FBTyxFQUFFLEVBQUUsRUFBRSxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBWSxFQUFFLENBQUE7UUFDdkMsQ0FBQztRQUVELE9BQU8sSUFBSSxDQUFBO0lBQ2IsQ0FBQztJQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7UUFDZixPQUFPLENBQUMsS0FBSyxDQUFDLDBEQUEwRCxFQUFFLEtBQUssQ0FBQyxDQUFBO1FBQ2hGLE9BQU8sSUFBSSxDQUFBO0lBQ2IsQ0FBQztBQUNILENBQUMifQ==
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateNumericOTP = generateNumericOTP;
4
+ exports.generateAlphanumericOTP = generateAlphanumericOTP;
5
+ /**
6
+ * Generate numeric OTP
7
+ */
8
+ function generateNumericOTP(length) {
9
+ const min = Math.pow(10, length - 1);
10
+ const max = Math.pow(10, length) - 1;
11
+ const otp = Math.floor(Math.random() * (max - min + 1)) + min;
12
+ return otp.toString();
13
+ }
14
+ /**
15
+ * Generate alphanumeric OTP (digits + consonants, excluding vowels and ambiguous characters)
16
+ */
17
+ function generateAlphanumericOTP(length) {
18
+ // Use digits and consonants only (excluding vowels and ambiguous characters like 0, O, I, 1, l)
19
+ const charset = "23456789BCDFGHJKLMNPQRSTVWXYZ";
20
+ let otp = "";
21
+ for (let i = 0; i < length; i++) {
22
+ const randomIndex = Math.floor(Math.random() * charset.length);
23
+ otp += charset[randomIndex];
24
+ }
25
+ return otp;
26
+ }
27
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3RwLWdlbmVyYXRvci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy91dGlscy9vdHAtZ2VuZXJhdG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBR0EsZ0RBS0M7QUFLRCwwREFXQztBQXhCRDs7R0FFRztBQUNILFNBQWdCLGtCQUFrQixDQUFDLE1BQWM7SUFDL0MsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLEVBQUUsTUFBTSxHQUFHLENBQUMsQ0FBQyxDQUFBO0lBQ3BDLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxFQUFFLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQTtJQUNwQyxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxNQUFNLEVBQUUsR0FBRyxDQUFDLEdBQUcsR0FBRyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUMsR0FBRyxHQUFHLENBQUE7SUFDN0QsT0FBTyxHQUFHLENBQUMsUUFBUSxFQUFFLENBQUE7QUFDdkIsQ0FBQztBQUVEOztHQUVHO0FBQ0gsU0FBZ0IsdUJBQXVCLENBQUMsTUFBYztJQUNwRCxnR0FBZ0c7SUFDaEcsTUFBTSxPQUFPLEdBQUcsK0JBQStCLENBQUE7SUFDL0MsSUFBSSxHQUFHLEdBQUcsRUFBRSxDQUFBO0lBRVosS0FBSyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxHQUFHLE1BQU0sRUFBRSxDQUFDLEVBQUUsRUFBRSxDQUFDO1FBQ2hDLE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxHQUFHLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQTtRQUM5RCxHQUFHLElBQUksT0FBTyxDQUFDLFdBQVcsQ0FBQyxDQUFBO0lBQzdCLENBQUM7SUFFRCxPQUFPLEdBQUcsQ0FBQTtBQUNaLENBQUMifQ==
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateToken = generateToken;
4
+ const crypto_1 = require("crypto");
5
+ /**
6
+ * Generate unique token for OTP record
7
+ */
8
+ function generateToken() {
9
+ return (0, crypto_1.randomBytes)(32).toString("hex");
10
+ }
11
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9rZW4tZ2VuZXJhdG9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3V0aWxzL3Rva2VuLWdlbmVyYXRvci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUtBLHNDQUVDO0FBUEQsbUNBQW9DO0FBRXBDOztHQUVHO0FBQ0gsU0FBZ0IsYUFBYTtJQUMzQixPQUFPLElBQUEsb0JBQVcsRUFBQyxFQUFFLENBQUMsQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUE7QUFDeEMsQ0FBQyJ9
package/README.md CHANGED
@@ -4,7 +4,7 @@ A lean Medusa v2 plugin that hardens customer onboarding, guarantees both `email
4
4
 
5
5
  ## Features
6
6
 
7
- - Overrides `POST /store/customers` with a drop-in route handler.
7
+ - Listens to Medusa's native `customer.created` event, so OTP automation works without overriding `POST /store/customers`.
8
8
  - Forces `email_verified = false` and `phone_verified = false` for every new customer, regardless of the request payload.
9
9
  - Automatically generates & sends an email OTP right after registration (configurable) using the Notification module + whatever providers your project already registered.
10
10
  - Adds dedicated email + phone OTP endpoints for resending and verifying codes while marking the corresponding `*_verified` flags to `true`.
@@ -54,52 +54,176 @@ yarn dev
54
54
 
55
55
  ## Usage
56
56
 
57
- ### Registration override
57
+ ### Registration lifecycle hook
58
58
 
59
- When a shopper registers via `POST /store/customers`, the plugin intercepts the request, normalizes the payload, and always persists `email_verified = false` and `phone_verified = false`. The rest of Medusa’s default behavior is preserved by deferring to the Customer Module service. If `email.autoSendOnRegistration` is enabled (default), the plugin immediately issues an email OTP through the Notification module using the user's configured providers.
59
+ The plugin no longer overrides `POST /store/customers`. Instead, it listens to the `customer.created` event and automatically issues an email OTP (when `email.autoSendOnRegistration` is enabled). Because the default Medusa route still handles persistence and response formatting, there are no behavioral differences for registration requests aside from the verification guard.
60
60
 
61
61
  ### OTP endpoints
62
62
 
63
- The plugin adds a tiny surface area of Store API routes so storefronts can request and verify codes without touching the Admin API:
63
+ Expose these Store API routes to your frontend so shoppers can complete verification without touching the Admin API:
64
64
 
65
- | Endpoint | Body | Description |
65
+ | Endpoint | Request Body | Description |
66
66
  | --- | --- | --- |
67
- | `POST /store/customers/email/otp/verify` | `{ email?: string, customer_id?: string, code: string }` | Validates an email OTP and sets `email_verified = true`. |
68
- | `POST /store/customers/phone/otp/send` | `{ email?: string, customer_id?: string }` | Generates an OTP and sends it to the customer's phone number (stored on the customer record). |
69
- | `POST /store/customers/phone/otp/verify` | `{ email?: string, customer_id?: string, code: string }` | Validates a phone OTP and sets `phone_verified = true`. |
67
+ | `POST /store/customers/email/otp/verify` | `{ email?: string; customer_id?: string; code: string }` | Confirms the latest email OTP and flips `email_verified` to `true`. |
68
+ | `POST /store/customers/phone/otp/send` | `{ email?: string; customer_id?: string }` | Generates a new OTP, throttled per config, and sends it via your configured SMS channel. |
69
+ | `POST /store/customers/phone/otp/verify` | `{ email?: string; customer_id?: string; code: string }` | Confirms the latest phone OTP and flips `phone_verified` to `true`. |
70
70
 
71
- All notification dispatches go through Medusa's Notification module. Whatever providers / templates you configure globally will be reused by the plugin.
71
+ All notification dispatches go through Medusa's Notification module, so whichever providers/templates your project already registers will be used automatically.
72
72
 
73
- ### Configuration
73
+ #### Example: send phone OTP
74
74
 
75
- You can override OTP behavior by passing options when registering the plugin:
75
+ Request:
76
+
77
+ ```http
78
+ POST /store/customers/phone/otp/send
79
+ Content-Type: application/json
80
+
81
+ {
82
+ "email": "sarah@example.com"
83
+ }
84
+ ```
85
+
86
+ Response:
87
+
88
+ ```json
89
+ {
90
+ "sent": true
91
+ }
92
+ ```
93
+
94
+ #### Example: verify email OTP
95
+
96
+ Request:
97
+
98
+ ```http
99
+ POST /store/customers/email/otp/verify
100
+ Content-Type: application/json
101
+
102
+ {
103
+ "customer_id": "cus_01J7J6PSJ8K7XR31R3Q0H0RZ5Q",
104
+ "code": "529104"
105
+ }
106
+ ```
107
+
108
+ Response:
109
+
110
+ ```json
111
+ {
112
+ "customer": {
113
+ "id": "cus_01J7J6PSJ8K7XR31R3Q0H0RZ5Q",
114
+ "email": "sarah@example.com",
115
+ "phone": "+12025550123",
116
+ "email_verified": true,
117
+ "phone_verified": false,
118
+ "...": "other Medusa customer fields"
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### Typical storefront flow
124
+
125
+ 1. **Register** – call `POST /store/customers` as usual (plugin sets both verification flags to `false`).
126
+ 2. **Auto email OTP (optional)** – if `email.autoSendOnRegistration` is `true`, the plugin immediately emails the code.
127
+ 3. **Resend OTP** – expose “Resend email/phone code” buttons that hit `POST /store/customers/email/otp/verify` or `POST /store/customers/phone/otp/send`.
128
+ 4. **Verify** – when the user submits the code, call the corresponding verify endpoint; the plugin marks the customer as verified and returns the updated customer object.
129
+
130
+ ### End-to-end OTP integration guide
131
+
132
+ Follow this sequence to enforce verification before login. Each step shows the minimal payloads and cURL commands your storefront or test suite can run.
133
+
134
+ 1. **Register the customer (no cookies returned)**
135
+ ```bash
136
+ curl -X POST http://localhost:9000/store/customers \
137
+ -H "Content-Type: application/json" \
138
+ -d '{
139
+ "first_name": "Ava",
140
+ "last_name": "Lopez",
141
+ "email": "ava@example.com",
142
+ "password": "S3cret!123"
143
+ }'
144
+ ```
145
+ The response contains `customer.email_verified = false`. If `email.autoSendOnRegistration` is enabled, an OTP is already dispatched (and logged to the console when no notification module is configured).
146
+
147
+ 2. **Verify the email OTP**
148
+ ```bash
149
+ curl -X POST http://localhost:9000/store/customers/email/otp/verify \
150
+ -H "Content-Type: application/json" \
151
+ -d '{
152
+ "email": "ava@example.com",
153
+ "code": "123456"
154
+ }'
155
+ ```
156
+ On success the returned customer now has `email_verified = true`. If an incorrect or expired code is provided, you receive a `400` or `403` style Medusa error describing the reason and you can prompt the shopper to retry.
157
+
158
+ 3. **Login via Medusa auth endpoint (email verification enforced)**
159
+ The plugin overrides `POST /auth/customer/emailpass` so tokens issue only when `email_verified` is true.
160
+ ```bash
161
+ curl -X POST http://localhost:9000/auth/customer/emailpass \
162
+ -H "Content-Type: application/json" \
163
+ -d '{
164
+ "email": "ava@example.com",
165
+ "password": "S3cret!123"
166
+ }'
167
+ ```
168
+ - If the email is verified, the response matches Medusa core (`{ "token": "..." }`).
169
+ - If not verified, the route returns `401` with `message: "Please verify your email before logging in."`
170
+
171
+ 4. **(Optional) Phone verification gating**
172
+ - During registration and OTP verification responses you always receive the latest `phone_verified` flag alongside `email_verified`.
173
+ - In an account or profile screen, read these flags from `GET /store/customers/me`. When `phone_verified` is `false`, show a CTA that calls `POST /store/customers/phone/otp/send` and `POST /store/customers/phone/otp/verify` just like the email flow.
174
+ - Once both flags are true, hide the prompts or replace them with a “Verified” badge.
175
+
176
+ > Hint: For QA environments without providers, OTP codes are logged with the `[customer-registration] OTP dispatch fallback` prefix so testers can copy/paste them while still exercising the full API surface.
177
+
178
+ ### Configuration reference
179
+
180
+ Declare the plugin inside `medusa-config.ts` with the options that match your UX and provider setup:
76
181
 
77
182
  ```ts
78
- plugins: [
79
- {
80
- resolve: "customer-registration",
81
- options: {
82
- otpLength: 6,
83
- otpCharset: "numeric", // or "alphanumeric"
84
- otpExpiryMinutes: 15,
85
- maxAttempts: 5,
86
- email: {
87
- channel: "email",
88
- template: "otp-template", // optional provider template handle
89
- subject: "Verify your Medusa account",
90
- resendThrottleSeconds: 90,
91
- autoSendOnRegistration: true,
92
- },
93
- phone: {
94
- channel: "sms",
95
- template: null,
96
- resendThrottleSeconds: 60,
183
+ import { defineConfig } from "@medusajs/framework/utils"
184
+
185
+ export default defineConfig({
186
+ // ...
187
+ plugins: [
188
+ {
189
+ resolve: "customer-registration",
190
+ options: {
191
+ otpLength: 6,
192
+ otpCharset: "numeric", // "numeric" or "alphanumeric"
193
+ otpExpiryMinutes: 15,
194
+ maxAttempts: 5,
195
+ email: {
196
+ channel: "email", // Medusa notification channel key
197
+ template: "otp-email-verify", // optional provider template
198
+ subject: "Verify your Medusa account",
199
+ resendThrottleSeconds: 90,
200
+ autoSendOnRegistration: true,
201
+ },
202
+ phone: {
203
+ channel: "sms",
204
+ template: "otp-phone-verify",
205
+ resendThrottleSeconds: 60,
206
+ },
97
207
  },
98
208
  },
99
- },
100
- ]
209
+ ],
210
+ })
101
211
  ```
102
212
 
213
+ | Option | Type | Description |
214
+ | --- | --- | --- |
215
+ | `otpLength` | `number` | Total characters generated per OTP. |
216
+ | `otpCharset` | `"numeric" \| "alphanumeric"` | Controls whether codes contain digits only or digits + consonants. |
217
+ | `otpExpiryMinutes` | `number` | Minutes before each OTP expires. |
218
+ | `maxAttempts` | `number` | Number of failed attempts before forcing a resend. |
219
+ | `email.channel` / `phone.channel` | `string` | Notification channel key registered in Medusa (e.g., `email`, `sms`). |
220
+ | `email.template` / `phone.template` | `string \| null` | Optional provider template handle (SendGrid ID, Twilio template, etc.). |
221
+ | `email.subject` | `string` | Fallback subject when sending raw email content. |
222
+ | `email.autoSendOnRegistration` | `boolean` | Auto-dispatch an email OTP immediately after registration. |
223
+ | `email.resendThrottleSeconds` / `phone.resendThrottleSeconds` | `number` | Minimum seconds before another OTP can be requested for the same purpose. |
224
+
225
+ > Tip: leave `template` as `null` to fall back to the built-in plain-text message content; otherwise provide the template ID your provider expects.
226
+
103
227
  ### Database migrations
104
228
 
105
229
  - `Migration20250118000000AddEmailVerifiedColumn`: ensures the customer table includes `email_verified` and `phone_verified` booleans.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "customer-registration",
3
- "version": "0.0.23",
3
+ "version": "0.0.47",
4
4
  "description": "Medusa plugin that overrides store customer registration and enforces email/phone verification flags.",
5
5
  "author": "Medusa (https://medusajs.com)",
6
6
  "license": "MIT",
@@ -30,6 +30,8 @@
30
30
  "@medusajs/cli": "2.11.2",
31
31
  "@medusajs/framework": "2.11.2",
32
32
  "@medusajs/medusa": "2.11.2",
33
+ "@types/bcryptjs": "^2.4.6",
34
+ "bcryptjs": "^2.4.3",
33
35
  "ts-node": "^10.9.2",
34
36
  "typescript": "^5.6.2",
35
37
  "vitest": "^1.5.2"
@@ -1,48 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.POST = POST;
4
- const utils_1 = require("@medusajs/framework/utils");
5
- const customer_registration_1 = require("../../../../../../modules/customer-registration");
6
- async function POST(req, res) {
7
- const { customer_id, email } = req.body;
8
- if (!customer_id && !email) {
9
- res.status(400).json({
10
- message: "Either customer_id or email is required to send a phone OTP.",
11
- type: "invalid_data",
12
- });
13
- return;
14
- }
15
- const customerRegistrationService = req.scope.resolve(customer_registration_1.CUSTOMER_REGISTRATION_MODULE);
16
- try {
17
- await customerRegistrationService.sendPhoneOtp({
18
- customerId: customer_id,
19
- email,
20
- });
21
- res.status(200).json({ sent: true });
22
- }
23
- catch (error) {
24
- if (utils_1.MedusaError.isMedusaError?.(error)) {
25
- res.status(mapMedusaErrorToStatus(error.type)).json({
26
- message: error.message,
27
- type: error.type,
28
- });
29
- return;
30
- }
31
- res.status(500).json({
32
- message: error?.message ?? "Failed to send phone OTP",
33
- type: "unknown_error",
34
- });
35
- }
36
- }
37
- const mapMedusaErrorToStatus = (type) => {
38
- switch (type) {
39
- case utils_1.MedusaError.Types.NOT_FOUND:
40
- return 404;
41
- case utils_1.MedusaError.Types.NOT_ALLOWED:
42
- case utils_1.MedusaError.Types.INVALID_DATA:
43
- return 400;
44
- default:
45
- return 500;
46
- }
47
- };
48
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL3N0b3JlL2N1c3RvbWVycy9waG9uZS9vdHAvc2VuZC9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQVlBLG9CQW9DQztBQS9DRCxxREFBdUQ7QUFDdkQsMkZBRXdEO0FBUWpELEtBQUssVUFBVSxJQUFJLENBQUMsR0FBa0IsRUFBRSxHQUFtQjtJQUNoRSxNQUFNLEVBQUUsV0FBVyxFQUFFLEtBQUssRUFBRSxHQUFHLEdBQUcsQ0FBQyxJQUF3QixDQUFBO0lBRTNELElBQUksQ0FBQyxXQUFXLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUMzQixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztZQUNuQixPQUFPLEVBQUUsOERBQThEO1lBQ3ZFLElBQUksRUFBRSxjQUFjO1NBQ3JCLENBQUMsQ0FBQTtRQUNGLE9BQU07SUFDUixDQUFDO0lBRUQsTUFBTSwyQkFBMkIsR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FDbkQsb0RBQTRCLENBQ1EsQ0FBQTtJQUV0QyxJQUFJLENBQUM7UUFDSCxNQUFNLDJCQUEyQixDQUFDLFlBQVksQ0FBQztZQUM3QyxVQUFVLEVBQUUsV0FBVztZQUN2QixLQUFLO1NBQ04sQ0FBQyxDQUFBO1FBRUYsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQTtJQUN0QyxDQUFDO0lBQUMsT0FBTyxLQUFVLEVBQUUsQ0FBQztRQUNwQixJQUFJLG1CQUFXLENBQUMsYUFBYSxFQUFFLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztZQUN2QyxHQUFHLENBQUMsTUFBTSxDQUFDLHNCQUFzQixDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQztnQkFDbEQsT0FBTyxFQUFFLEtBQUssQ0FBQyxPQUFPO2dCQUN0QixJQUFJLEVBQUUsS0FBSyxDQUFDLElBQUk7YUFDakIsQ0FBQyxDQUFBO1lBQ0YsT0FBTTtRQUNSLENBQUM7UUFFRCxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztZQUNuQixPQUFPLEVBQUUsS0FBSyxFQUFFLE9BQU8sSUFBSSwwQkFBMEI7WUFDckQsSUFBSSxFQUFFLGVBQWU7U0FDdEIsQ0FBQyxDQUFBO0lBQ0osQ0FBQztBQUNILENBQUM7QUFFRCxNQUFNLHNCQUFzQixHQUFHLENBQUMsSUFBWSxFQUFFLEVBQUU7SUFDOUMsUUFBUSxJQUFJLEVBQUUsQ0FBQztRQUNiLEtBQUssbUJBQVcsQ0FBQyxLQUFLLENBQUMsU0FBUztZQUM5QixPQUFPLEdBQUcsQ0FBQTtRQUNaLEtBQUssbUJBQVcsQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDO1FBQ25DLEtBQUssbUJBQVcsQ0FBQyxLQUFLLENBQUMsWUFBWTtZQUNqQyxPQUFPLEdBQUcsQ0FBQTtRQUNaO1lBQ0UsT0FBTyxHQUFHLENBQUE7SUFDZCxDQUFDO0FBQ0gsQ0FBQyxDQUFBIn0=