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.
- package/.medusa/server/src/api/auth/customer/emailpass/route.js +58 -0
- package/.medusa/server/src/api/store/customers/email/otp/resend/route.js +78 -0
- package/.medusa/server/src/api/store/customers/email/otp/verify/route.js +54 -40
- package/.medusa/server/src/api/store/customers/forget-password/otp/resend/route.js +52 -0
- package/.medusa/server/src/api/store/customers/forget-password/otp/verify/route.js +42 -0
- package/.medusa/server/src/api/store/customers/phone/otp/resend/route.js +81 -0
- package/.medusa/server/src/api/store/customers/phone/otp/verify/route.js +60 -41
- package/.medusa/server/src/errors/otp-errors.js +29 -0
- package/.medusa/server/src/loaders/index.js +29 -9
- package/.medusa/server/src/modules/customer-registration/index.js +43 -9
- package/.medusa/server/src/modules/customer-registration/migrations/Migration20251122112915AddEmailPhoneVerifiedColumns.js +67 -0
- package/.medusa/server/src/modules/customer-registration/migrations/Migration20251122112916CreateCustomerOtpTable.js +56 -0
- package/.medusa/server/src/modules/customer-registration/models/customer-otp.js +65 -32
- package/.medusa/server/src/modules/customer-registration/services/otp-service.js +226 -0
- package/.medusa/server/src/services/notification-service.js +81 -0
- package/.medusa/server/src/subscribers/customer-created.js +42 -0
- package/.medusa/server/src/types/plugin-options.js +30 -0
- package/.medusa/server/src/utils/crypto.js +52 -0
- package/.medusa/server/src/utils/customer-update.js +48 -0
- package/.medusa/server/src/utils/otp-generator.js +27 -0
- package/.medusa/server/src/utils/token-generator.js +11 -0
- package/README.md +156 -32
- package/package.json +3 -1
- package/.medusa/server/src/api/store/customers/phone/otp/send/route.js +0 -48
- package/.medusa/server/src/api/store/customers/route.js +0 -77
- package/.medusa/server/src/modules/customer-registration/__tests__/config.spec.js +0 -61
- package/.medusa/server/src/modules/customer-registration/config.js +0 -73
- package/.medusa/server/src/modules/customer-registration/constants.js +0 -5
- package/.medusa/server/src/modules/customer-registration/migrations/Migration20250118000000AddEmailVerifiedColumn.js +0 -21
- package/.medusa/server/src/modules/customer-registration/migrations/Migration20250118001000CreateCustomerOtpTable.js +0 -48
- package/.medusa/server/src/modules/customer-registration/service.js +0 -242
- 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
|
-
-
|
|
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
|
|
57
|
+
### Registration lifecycle hook
|
|
58
58
|
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
68
|
-
| `POST /store/customers/phone/otp/send` | `{ email?: string
|
|
69
|
-
| `POST /store/customers/phone/otp/verify` | `{ email?: string
|
|
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
|
|
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
|
-
|
|
73
|
+
#### Example: send phone OTP
|
|
74
74
|
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
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=
|