@webbycrown/webbycommerce 1.2.0 → 2.0.0
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/README.md +26 -3
- package/admin/app.js +3 -0
- package/admin/jsconfig.json +20 -0
- package/admin/src/components/ApiCollectionsContent.jsx +4626 -0
- package/admin/src/components/CompareContent.jsx +300 -0
- package/admin/src/components/ConfigureContent.jsx +407 -0
- package/admin/src/components/Initializer.jsx +64 -0
- package/admin/src/components/LoginRegisterContent.jsx +280 -0
- package/admin/src/components/PluginIcon.jsx +6 -0
- package/admin/src/components/ShippingTypeContent.jsx +230 -0
- package/admin/src/components/SmtpContent.jsx +316 -0
- package/admin/src/components/WishlistContent.jsx +273 -0
- package/admin/src/index.js +81 -0
- package/admin/src/pages/ApiCollections.jsx +169 -0
- package/admin/src/pages/Configure.jsx +55 -0
- package/admin/src/pages/Settings.jsx +93 -0
- package/admin/src/pluginId.js +4 -0
- package/{dist/_chunks/en-CiQ97iC8.js → admin/src/translations/en.json} +712 -574
- package/bin/setup.js +50 -3
- package/package.json +14 -13
- package/server/bootstrap.js +3 -0
- package/server/register.js +3 -0
- package/server/src/bootstrap.js +3826 -0
- package/server/src/components/content-block.json +37 -0
- package/server/src/components/shipping-zone-location.json +27 -0
- package/server/src/config/index.js +7 -0
- package/server/src/content-types/address/index.js +7 -0
- package/server/src/content-types/address/schema.json +74 -0
- package/server/src/content-types/cart/index.js +61 -0
- package/server/src/content-types/cart-item/index.js +79 -0
- package/server/src/content-types/compare.js +73 -0
- package/server/src/content-types/coupon/index.js +7 -0
- package/server/src/content-types/coupon/schema.json +67 -0
- package/server/src/content-types/index.js +42 -0
- package/server/src/content-types/order/index.js +7 -0
- package/server/src/content-types/order/schema.json +121 -0
- package/server/src/content-types/payment-transaction/index.js +7 -0
- package/server/src/content-types/payment-transaction/schema.json +73 -0
- package/server/src/content-types/product/index.js +7 -0
- package/server/src/content-types/product/schema.json +104 -0
- package/server/src/content-types/product-attribute/index.js +7 -0
- package/server/src/content-types/product-attribute/schema.json +80 -0
- package/server/src/content-types/product-attribute-value/index.js +7 -0
- package/server/src/content-types/product-attribute-value/schema.json +52 -0
- package/server/src/content-types/product-category/index.js +7 -0
- package/server/src/content-types/product-category/schema.json +54 -0
- package/server/src/content-types/product-tag/index.js +7 -0
- package/server/src/content-types/product-tag/schema.json +38 -0
- package/server/src/content-types/product-variation/index.js +7 -0
- package/server/src/content-types/product-variation/schema.json +74 -0
- package/server/src/content-types/shipping-method/index.js +7 -0
- package/server/src/content-types/shipping-method/schema.json +91 -0
- package/server/src/content-types/shipping-rate/index.js +7 -0
- package/server/src/content-types/shipping-rate/schema.json +73 -0
- package/server/src/content-types/shipping-rule/index.js +7 -0
- package/server/src/content-types/shipping-rule/schema.json +84 -0
- package/server/src/content-types/shipping-zone/index.js +7 -0
- package/server/src/content-types/shipping-zone/schema.json +57 -0
- package/server/src/content-types/wishlist.js +66 -0
- package/server/src/controllers/address.js +374 -0
- package/server/src/controllers/auth.js +1409 -0
- package/server/src/controllers/cart.js +337 -0
- package/server/src/controllers/category.js +388 -0
- package/server/src/controllers/compare.js +246 -0
- package/server/src/controllers/controller.js +168 -0
- package/server/src/controllers/ecommerce.js +20 -0
- package/server/src/controllers/index.js +34 -0
- package/server/src/controllers/order.js +1100 -0
- package/server/src/controllers/payment.js +243 -0
- package/server/src/controllers/product.js +1006 -0
- package/server/src/controllers/productTag.js +370 -0
- package/server/src/controllers/productVariation.js +181 -0
- package/server/src/controllers/shipping.js +1046 -0
- package/server/src/controllers/wishlist.js +332 -0
- package/server/src/destroy.js +6 -0
- package/server/src/index.js +26 -0
- package/server/src/middlewares/index.js +4 -0
- package/server/src/policies/index.js +4 -0
- package/server/src/register.js +67 -0
- package/server/src/routes/index.js +1130 -0
- package/server/src/services/cart.js +531 -0
- package/server/src/services/compare.js +300 -0
- package/server/src/services/index.js +16 -0
- package/server/src/services/service.js +19 -0
- package/server/src/services/shipping.js +513 -0
- package/server/src/services/wishlist.js +238 -0
- package/server/src/utils/check-ecommerce-permission.js +204 -0
- package/server/src/utils/extend-user-schema.js +161 -0
- package/server/src/utils/seed-data.js +639 -0
- package/server/src/utils/send-email.js +98 -0
- package/strapi-server.js +1 -6
- package/dist/_chunks/Settings-DZXAkI24.js +0 -31539
- package/dist/_chunks/Settings-yLx-YvVy.mjs +0 -31520
- package/dist/_chunks/en-DE15m4xZ.mjs +0 -574
- package/dist/_chunks/index-CXGrFKp6.mjs +0 -128
- package/dist/_chunks/index-DgocXUgC.js +0 -127
- package/dist/admin/index.js +0 -3
- package/dist/admin/index.mjs +0 -4
- package/dist/robots.txt +0 -3
- package/dist/server/index.js +0 -27078
- package/dist/uploads/.gitkeep +0 -0
- package/dist/uploads/accessories_category_2a5631094b.jpeg +0 -0
- package/dist/uploads/beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
- package/dist/uploads/books_category_a9a253eada.jpeg +0 -0
- package/dist/uploads/classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/clothing_category_d5c60ef07b.jpeg +0 -0
- package/dist/uploads/daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/electronics_category_fc3e5ef571.jpeg +0 -0
- package/dist/uploads/ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/home_garden_category_4f6eb3f8d6.jpeg +0 -0
- package/dist/uploads/istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
- package/dist/uploads/istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
- package/dist/uploads/large_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/medium_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/medium_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/medium_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/medium_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/medium_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/medium_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/medium_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/medium_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/medium_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/medium_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
- package/dist/uploads/small_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/small_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/small_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/small_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/small_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/small_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/small_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/small_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/small_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/small_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/thumbnail_accessories_category_2a5631094b.jpeg +0 -0
- package/dist/uploads/thumbnail_beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
- package/dist/uploads/thumbnail_books_category_a9a253eada.jpeg +0 -0
- package/dist/uploads/thumbnail_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/thumbnail_clothing_category_d5c60ef07b.jpeg +0 -0
- package/dist/uploads/thumbnail_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/thumbnail_electronics_category_fc3e5ef571.jpeg +0 -0
- package/dist/uploads/thumbnail_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/thumbnail_home_garden_category_4f6eb3f8d6.jpeg +0 -0
- package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
- package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
- package/dist/uploads/thumbnail_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/thumbnail_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/thumbnail_predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
- package/dist/uploads/thumbnail_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/thumbnail_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/thumbnail_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/thumbnail_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/thumbnail_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/webby-commerce.png +0 -0
- package/dist/uploads/wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- /package/{dist → server/src}/data/demo-data.json +0 -0
|
@@ -0,0 +1,1409 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PLUGIN_ID = 'webbycommerce';
|
|
4
|
+
const bcrypt = require('bcryptjs');
|
|
5
|
+
const { sendEmail } = require('../utils/send-email');
|
|
6
|
+
const { ensureEcommercePermission } = require('../utils/check-ecommerce-permission');
|
|
7
|
+
|
|
8
|
+
const getStore = () => {
|
|
9
|
+
return strapi.store({ type: 'plugin', name: PLUGIN_ID });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const getLoginMethod = async () => {
|
|
13
|
+
const store = getStore();
|
|
14
|
+
const value = (await store.get({ key: 'settings' })) || {};
|
|
15
|
+
return value.loginRegisterMethod || 'default';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
/**
|
|
20
|
+
* Login or Register with OTP
|
|
21
|
+
* Supports both email and mobile number
|
|
22
|
+
*/
|
|
23
|
+
async loginOrRegister(ctx) {
|
|
24
|
+
try {
|
|
25
|
+
// Check ecommerce permission first
|
|
26
|
+
const hasPermission = await ensureEcommercePermission(ctx);
|
|
27
|
+
if (!hasPermission) {
|
|
28
|
+
return; // ensureEcommercePermission already sent the response
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check if OTP method is enabled
|
|
32
|
+
const method = await getLoginMethod();
|
|
33
|
+
if (method !== 'otp') {
|
|
34
|
+
return ctx.badRequest('OTP authentication is not enabled. Please enable it in plugin settings.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { email, mobile, type } = ctx.request.body;
|
|
38
|
+
|
|
39
|
+
// Normalize email for case-insensitive match
|
|
40
|
+
let normalizedEmail = email?.toLowerCase();
|
|
41
|
+
|
|
42
|
+
if (!type || (type !== 'email' && type !== 'mobile')) {
|
|
43
|
+
return ctx.badRequest('Type must be "email" or "mobile".');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let identifier = type === 'email' ? normalizedEmail : mobile;
|
|
47
|
+
|
|
48
|
+
if (!identifier) {
|
|
49
|
+
return ctx.badRequest(
|
|
50
|
+
`${type === 'email' ? 'Email' : 'Mobile number'} is required.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Find user
|
|
55
|
+
let user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
56
|
+
where: type === 'email' ? { email: normalizedEmail } : { phone_no: mobile },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let isNewUser = false;
|
|
60
|
+
|
|
61
|
+
// Create user if not exists
|
|
62
|
+
if (!user) {
|
|
63
|
+
let username = '';
|
|
64
|
+
|
|
65
|
+
if (type === 'email' && email) {
|
|
66
|
+
// EMAIL USERNAME — exactly 8 chars (letters + numbers)
|
|
67
|
+
let base = email.split('@')[0].replace(/[^a-zA-Z]/g, '').toLowerCase();
|
|
68
|
+
if (!base) base = 'user';
|
|
69
|
+
|
|
70
|
+
base = base.substring(0, 4); // max 4 letters
|
|
71
|
+
|
|
72
|
+
const digitsNeeded = 8 - base.length;
|
|
73
|
+
const min = Math.pow(10, digitsNeeded - 1);
|
|
74
|
+
const max = Math.pow(10, digitsNeeded) - 1;
|
|
75
|
+
|
|
76
|
+
let randomDigits = String(Math.floor(Math.random() * (max - min + 1)) + min);
|
|
77
|
+
|
|
78
|
+
username = (base + randomDigits).substring(0, 8);
|
|
79
|
+
} else if (type === 'mobile' && mobile) {
|
|
80
|
+
// MOBILE USERNAME — MUST start with "webby" and be exactly 8 chars
|
|
81
|
+
const prefix = 'webby'; // length 5
|
|
82
|
+
|
|
83
|
+
// Need exactly 3 digits
|
|
84
|
+
const randomDigits = String(Math.floor(100 + Math.random() * 900)); // 100–999
|
|
85
|
+
|
|
86
|
+
username = prefix + randomDigits; // total = 8 chars
|
|
87
|
+
} else {
|
|
88
|
+
username = 'user' + String(Math.floor(1000 + Math.random() * 9000)); // fallback 8 chars
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Ensure unique
|
|
92
|
+
const existing = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
93
|
+
where: { username },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (existing) {
|
|
97
|
+
if (type === 'mobile') {
|
|
98
|
+
username = 'webby' + String(Math.floor(100 + Math.random() * 900));
|
|
99
|
+
} else {
|
|
100
|
+
username = username.substring(0, 4) + String(Math.floor(1000 + Math.random() * 9000));
|
|
101
|
+
username = username.substring(0, 8);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Get default role (usually Public role with ID 2)
|
|
106
|
+
const defaultRole = await strapi.db
|
|
107
|
+
.query('plugin::users-permissions.role')
|
|
108
|
+
.findOne({ where: { type: 'public' } });
|
|
109
|
+
|
|
110
|
+
// Create user
|
|
111
|
+
user = await strapi.plugin('users-permissions').service('user').add({
|
|
112
|
+
email: type === 'email' ? normalizedEmail : null,
|
|
113
|
+
phone_no: type === 'mobile' ? mobile : null,
|
|
114
|
+
username,
|
|
115
|
+
confirmed: false,
|
|
116
|
+
role: defaultRole?.id || 2,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
isNewUser = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Generate OTP (6 digits)
|
|
123
|
+
const otp = Math.floor(100000 + Math.random() * 900000);
|
|
124
|
+
const otpDigits = otp.toString().split('');
|
|
125
|
+
|
|
126
|
+
// Save OTP using raw query to bypass schema validation
|
|
127
|
+
// This allows us to use fields that may exist in DB but not in schema
|
|
128
|
+
try {
|
|
129
|
+
const db = strapi.db;
|
|
130
|
+
const knex = db.connection;
|
|
131
|
+
const tableName = 'up_users';
|
|
132
|
+
const client = db.config.connection.client;
|
|
133
|
+
|
|
134
|
+
// Use raw SQL to update OTP fields
|
|
135
|
+
if (client === 'postgres') {
|
|
136
|
+
await knex.raw(
|
|
137
|
+
`UPDATE ${tableName} SET otp = ?, is_otp_verified = ? WHERE id = ?`,
|
|
138
|
+
[otp, false, user.id]
|
|
139
|
+
);
|
|
140
|
+
} else if (client === 'mysql' || client === 'mysql2') {
|
|
141
|
+
await knex.raw(
|
|
142
|
+
`UPDATE \`${tableName}\` SET \`otp\` = ?, \`is_otp_verified\` = ? WHERE \`id\` = ?`,
|
|
143
|
+
[otp, false, user.id]
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
// SQLite
|
|
147
|
+
await knex.raw(
|
|
148
|
+
`UPDATE ${tableName} SET otp = ?, is_otp_verified = ? WHERE id = ?`,
|
|
149
|
+
[otp, false, user.id]
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
// If OTP fields don't exist in database, try to add them
|
|
154
|
+
strapi.log.warn(
|
|
155
|
+
`[${PLUGIN_ID}] OTP fields not found in database. Attempting to add them...`
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Try to extend schema and retry
|
|
159
|
+
try {
|
|
160
|
+
const { extendUserSchemaWithOtpFields } = require('../utils/extend-user-schema');
|
|
161
|
+
await extendUserSchemaWithOtpFields(strapi);
|
|
162
|
+
|
|
163
|
+
// Retry the update
|
|
164
|
+
const db = strapi.db;
|
|
165
|
+
const knex = db.connection;
|
|
166
|
+
const tableName = 'up_users';
|
|
167
|
+
const client = db.config.connection.client;
|
|
168
|
+
|
|
169
|
+
if (client === 'postgres') {
|
|
170
|
+
await knex.raw(
|
|
171
|
+
`UPDATE ${tableName} SET otp = ?, is_otp_verified = ? WHERE id = ?`,
|
|
172
|
+
[otp, false, user.id]
|
|
173
|
+
);
|
|
174
|
+
} else if (client === 'mysql' || client === 'mysql2') {
|
|
175
|
+
await knex.raw(
|
|
176
|
+
`UPDATE \`${tableName}\` SET \`otp\` = ?, \`is_otp_verified\` = ? WHERE \`id\` = ?`,
|
|
177
|
+
[otp, false, user.id]
|
|
178
|
+
);
|
|
179
|
+
} else {
|
|
180
|
+
await knex.raw(
|
|
181
|
+
`UPDATE ${tableName} SET otp = ?, is_otp_verified = ? WHERE id = ?`,
|
|
182
|
+
[otp, false, user.id]
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
} catch (retryErr) {
|
|
186
|
+
strapi.log.error(
|
|
187
|
+
`[${PLUGIN_ID}] Failed to add OTP fields: ${retryErr.message}`
|
|
188
|
+
);
|
|
189
|
+
throw new Error(
|
|
190
|
+
'OTP fields are not available in the user schema. Please extend the user schema as described in the plugin README.'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Send OTP
|
|
196
|
+
let emailSent = false;
|
|
197
|
+
|
|
198
|
+
if (type === 'email') {
|
|
199
|
+
// Send OTP via email
|
|
200
|
+
const otpEmailHTML = `
|
|
201
|
+
<!DOCTYPE html>
|
|
202
|
+
<html lang="en">
|
|
203
|
+
<head>
|
|
204
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
205
|
+
<meta charset="UTF-8" />
|
|
206
|
+
<title>OTP Confirmation</title>
|
|
207
|
+
</head>
|
|
208
|
+
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: Arial, sans-serif;">
|
|
209
|
+
<table align="center" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width: 650px; margin: 0 auto; background-color: #ffffff;">
|
|
210
|
+
<tr>
|
|
211
|
+
<td align="center" style="padding: 60px 20px 8px;">
|
|
212
|
+
<h1 style="font-weight: bold; color: #111; font-size: 32px; margin: 0;">Your OTP Code for Secure Access</h1>
|
|
213
|
+
</td>
|
|
214
|
+
</tr>
|
|
215
|
+
<tr>
|
|
216
|
+
<td align="center" style="padding: 16px 30px;">
|
|
217
|
+
<p style="font-size: 16px; color: #333333; line-height: 24px; margin: 0; max-width: 500px;">
|
|
218
|
+
To complete your account verification, please use the One-Time Password (OTP) provided below. For security reasons, this OTP will expire in 10 minutes and can only be used once.
|
|
219
|
+
</p>
|
|
220
|
+
</td>
|
|
221
|
+
</tr>
|
|
222
|
+
<tr>
|
|
223
|
+
<td align="center" style="padding: 30px 0;">
|
|
224
|
+
<table cellpadding="0" cellspacing="0" border="0" style="margin: 0 auto;">
|
|
225
|
+
<tr>
|
|
226
|
+
<td style="padding: 0 6px;">
|
|
227
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
228
|
+
<tr>
|
|
229
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[0]}</td>
|
|
230
|
+
</tr>
|
|
231
|
+
</table>
|
|
232
|
+
</td>
|
|
233
|
+
<td style="padding: 0 6px;">
|
|
234
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
235
|
+
<tr>
|
|
236
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[1]}</td>
|
|
237
|
+
</tr>
|
|
238
|
+
</table>
|
|
239
|
+
</td>
|
|
240
|
+
<td style="padding: 0 6px;">
|
|
241
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
242
|
+
<tr>
|
|
243
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[2]}</td>
|
|
244
|
+
</tr>
|
|
245
|
+
</table>
|
|
246
|
+
</td>
|
|
247
|
+
<td style="padding: 0 6px;">
|
|
248
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
249
|
+
<tr>
|
|
250
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[3]}</td>
|
|
251
|
+
</tr>
|
|
252
|
+
</table>
|
|
253
|
+
</td>
|
|
254
|
+
<td style="padding: 0 6px;">
|
|
255
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
256
|
+
<tr>
|
|
257
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[4]}</td>
|
|
258
|
+
</tr>
|
|
259
|
+
</table>
|
|
260
|
+
</td>
|
|
261
|
+
<td style="padding: 0 6px;">
|
|
262
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
263
|
+
<tr>
|
|
264
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[5]}</td>
|
|
265
|
+
</tr>
|
|
266
|
+
</table>
|
|
267
|
+
</td>
|
|
268
|
+
</tr>
|
|
269
|
+
</table>
|
|
270
|
+
</td>
|
|
271
|
+
</tr>
|
|
272
|
+
</table>
|
|
273
|
+
</body>
|
|
274
|
+
</html>
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
// Send OTP email using configured SMTP or fallback to Strapi's email plugin
|
|
279
|
+
await sendEmail({
|
|
280
|
+
to: email,
|
|
281
|
+
subject: 'Your OTP Code - Strapi WebbyCommerce',
|
|
282
|
+
html: otpEmailHTML,
|
|
283
|
+
});
|
|
284
|
+
emailSent = true;
|
|
285
|
+
} catch (emailError) {
|
|
286
|
+
// Do not block user creation/login if email fails; just log the error
|
|
287
|
+
strapi.log.error(
|
|
288
|
+
`[${PLUGIN_ID}] Failed to send OTP email (userId: ${user.id}, email: ${email}):`,
|
|
289
|
+
emailError
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
} else if (type === 'mobile') {
|
|
293
|
+
// Send OTP via SMS (requires SMS provider configuration)
|
|
294
|
+
// For now, we'll just log it. You can integrate with AWS SNS, Twilio, etc.
|
|
295
|
+
strapi.log.info(`[${PLUGIN_ID}] OTP for mobile ${mobile}: ${otp}`);
|
|
296
|
+
// TODO: Integrate with SMS provider (AWS SNS, Twilio, etc.)
|
|
297
|
+
// Example with AWS SNS:
|
|
298
|
+
// const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns');
|
|
299
|
+
// const snsClient = new SNSClient({ region: 'ap-south-1', credentials: {...} });
|
|
300
|
+
// await snsClient.send(new PublishCommand({
|
|
301
|
+
// Message: `Your OTP is ${otp}. Do not share this code.`,
|
|
302
|
+
// PhoneNumber: mobile,
|
|
303
|
+
// }));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
ctx.send({
|
|
307
|
+
message: emailSent
|
|
308
|
+
? `OTP sent to ${type}.`
|
|
309
|
+
: type === 'email'
|
|
310
|
+
? 'User created. OTP email could not be sent; please check email configuration on the server.'
|
|
311
|
+
: `OTP sent to ${type}.`, // mobile (future)
|
|
312
|
+
userId: user.id,
|
|
313
|
+
isNewUser,
|
|
314
|
+
emailSent,
|
|
315
|
+
});
|
|
316
|
+
} catch (error) {
|
|
317
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in loginOrRegister:`, error);
|
|
318
|
+
ctx.internalServerError('Failed to send OTP. Please try again.');
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Verify OTP and complete login/registration
|
|
324
|
+
*/
|
|
325
|
+
async verifyOtp(ctx) {
|
|
326
|
+
try {
|
|
327
|
+
// Check ecommerce permission first
|
|
328
|
+
const hasPermission = await ensureEcommercePermission(ctx);
|
|
329
|
+
if (!hasPermission) {
|
|
330
|
+
return; // ensureEcommercePermission already sent the response
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check if OTP method is enabled
|
|
334
|
+
const method = await getLoginMethod();
|
|
335
|
+
if (method !== 'otp') {
|
|
336
|
+
return ctx.badRequest('OTP authentication is not enabled. Please enable it in plugin settings.');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const { email, mobile, otp, type = 'email' } = ctx.request.body;
|
|
340
|
+
|
|
341
|
+
if (!otp || !((type === 'email' && email) || (type === 'mobile' && mobile))) {
|
|
342
|
+
return ctx.badRequest(
|
|
343
|
+
`${type === 'email' ? 'Email' : 'Mobile number'} and OTP are required.`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const identifier = type === 'email' ? email.toLowerCase() : mobile;
|
|
348
|
+
|
|
349
|
+
// Get user using ORM
|
|
350
|
+
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
351
|
+
where: { [type === 'email' ? 'email' : 'phone_no']: identifier },
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (!user) return ctx.badRequest('User not found.');
|
|
355
|
+
|
|
356
|
+
// Get OTP fields using raw query to bypass schema validation
|
|
357
|
+
const db = strapi.db;
|
|
358
|
+
const knex = db.connection;
|
|
359
|
+
const tableName = 'up_users';
|
|
360
|
+
const client = db.config.connection.client;
|
|
361
|
+
|
|
362
|
+
let userOtpData;
|
|
363
|
+
let columnsExist = false;
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
if (client === 'postgres') {
|
|
367
|
+
const result = await knex.raw(
|
|
368
|
+
`SELECT otp, is_otp_verified FROM ${tableName} WHERE id = ?`,
|
|
369
|
+
[user.id]
|
|
370
|
+
);
|
|
371
|
+
userOtpData = result.rows[0];
|
|
372
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
373
|
+
} else if (client === 'mysql' || client === 'mysql2') {
|
|
374
|
+
const result = await knex.raw(
|
|
375
|
+
`SELECT \`otp\`, \`is_otp_verified\` FROM \`${tableName}\` WHERE \`id\` = ?`,
|
|
376
|
+
[user.id]
|
|
377
|
+
);
|
|
378
|
+
userOtpData = result[0][0];
|
|
379
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
380
|
+
} else {
|
|
381
|
+
// SQLite
|
|
382
|
+
const result = await knex.raw(
|
|
383
|
+
`SELECT otp, is_otp_verified FROM ${tableName} WHERE id = ?`,
|
|
384
|
+
[user.id]
|
|
385
|
+
);
|
|
386
|
+
userOtpData = result[0];
|
|
387
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
388
|
+
}
|
|
389
|
+
} catch (queryErr) {
|
|
390
|
+
// If query fails (columns don't exist), try to add them
|
|
391
|
+
strapi.log.warn(`[${PLUGIN_ID}] OTP columns not found, attempting to add them:`, queryErr.message);
|
|
392
|
+
columnsExist = false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Check if columns exist, if not try to add them
|
|
396
|
+
if (!columnsExist) {
|
|
397
|
+
// Try to add the columns
|
|
398
|
+
const { extendUserSchemaWithOtpFields } = require('../utils/extend-user-schema');
|
|
399
|
+
const schemaExtended = await extendUserSchemaWithOtpFields(strapi);
|
|
400
|
+
|
|
401
|
+
if (!schemaExtended) {
|
|
402
|
+
return ctx.badRequest(
|
|
403
|
+
'OTP fields are not available in the user schema. Please extend the user schema as described in the plugin README.'
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Wait a moment for the schema changes to be committed
|
|
408
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
409
|
+
|
|
410
|
+
// Retry the query
|
|
411
|
+
try {
|
|
412
|
+
if (client === 'postgres') {
|
|
413
|
+
const result = await knex.raw(
|
|
414
|
+
`SELECT otp, is_otp_verified FROM ${tableName} WHERE id = ?`,
|
|
415
|
+
[user.id]
|
|
416
|
+
);
|
|
417
|
+
userOtpData = result.rows[0];
|
|
418
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
419
|
+
} else if (client === 'mysql' || client === 'mysql2') {
|
|
420
|
+
const result = await knex.raw(
|
|
421
|
+
`SELECT \`otp\`, \`is_otp_verified\` FROM \`${tableName}\` WHERE \`id\` = ?`,
|
|
422
|
+
[user.id]
|
|
423
|
+
);
|
|
424
|
+
userOtpData = result[0][0];
|
|
425
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
426
|
+
} else {
|
|
427
|
+
const result = await knex.raw(
|
|
428
|
+
`SELECT otp, is_otp_verified FROM ${tableName} WHERE id = ?`,
|
|
429
|
+
[user.id]
|
|
430
|
+
);
|
|
431
|
+
userOtpData = result[0];
|
|
432
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!columnsExist) {
|
|
436
|
+
return ctx.badRequest(
|
|
437
|
+
'OTP fields were added but could not be queried. Please restart Strapi.'
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
} catch (retryErr) {
|
|
441
|
+
strapi.log.error(`[${PLUGIN_ID}] Failed to query OTP fields after extension:`, retryErr);
|
|
442
|
+
return ctx.badRequest(
|
|
443
|
+
'OTP fields are not available. Please restart Strapi after extending the user schema.'
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const userOtp = userOtpData?.otp;
|
|
449
|
+
const isOtpVerified = userOtpData?.is_otp_verified;
|
|
450
|
+
|
|
451
|
+
if (isOtpVerified) return ctx.badRequest('User already verified.');
|
|
452
|
+
if (userOtp !== parseInt(otp, 10)) return ctx.badRequest('Invalid OTP.');
|
|
453
|
+
|
|
454
|
+
// Update user verification using raw query
|
|
455
|
+
try {
|
|
456
|
+
if (client === 'postgres') {
|
|
457
|
+
await knex.raw(
|
|
458
|
+
`UPDATE ${tableName} SET is_otp_verified = ?, confirmed = true, otp = NULL WHERE id = ?`,
|
|
459
|
+
[true, user.id]
|
|
460
|
+
);
|
|
461
|
+
} else if (client === 'mysql' || client === 'mysql2') {
|
|
462
|
+
await knex.raw(
|
|
463
|
+
`UPDATE \`${tableName}\` SET \`is_otp_verified\` = ?, \`confirmed\` = true, \`otp\` = NULL WHERE \`id\` = ?`,
|
|
464
|
+
[true, user.id]
|
|
465
|
+
);
|
|
466
|
+
} else {
|
|
467
|
+
await knex.raw(
|
|
468
|
+
`UPDATE ${tableName} SET is_otp_verified = ?, confirmed = 1, otp = NULL WHERE id = ?`,
|
|
469
|
+
[true, user.id]
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
} catch (dbErr) {
|
|
473
|
+
strapi.log.error(`[${PLUGIN_ID}] Database error during OTP verification:`, dbErr);
|
|
474
|
+
return ctx.badRequest(
|
|
475
|
+
'OTP fields are not available in the user schema. Please extend the user schema as described in the plugin README.'
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Generate JWT token
|
|
480
|
+
const jwt = strapi.plugins['users-permissions'].services.jwt.issue({
|
|
481
|
+
id: user.id,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
ctx.send({
|
|
485
|
+
message: 'Login successfully!',
|
|
486
|
+
jwt,
|
|
487
|
+
user: {
|
|
488
|
+
id: user.id,
|
|
489
|
+
username: user.username,
|
|
490
|
+
email: user.email,
|
|
491
|
+
phone_no: user.phone_no,
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
} catch (error) {
|
|
495
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in verifyOtp:`, error);
|
|
496
|
+
ctx.internalServerError('Failed to verify OTP. Please try again.');
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Get current authentication method
|
|
502
|
+
* Returns which authentication method is currently enabled (default or otp)
|
|
503
|
+
* This endpoint is public and can be used by frontend to determine which auth flow to use
|
|
504
|
+
*/
|
|
505
|
+
async getAuthMethod(ctx) {
|
|
506
|
+
try {
|
|
507
|
+
const method = await getLoginMethod();
|
|
508
|
+
|
|
509
|
+
ctx.send({
|
|
510
|
+
method,
|
|
511
|
+
message: `Current authentication method: ${method}`,
|
|
512
|
+
});
|
|
513
|
+
} catch (error) {
|
|
514
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getAuthMethod:`, error);
|
|
515
|
+
// Default to 'default' if there's an error reading settings
|
|
516
|
+
ctx.send({
|
|
517
|
+
method: 'default',
|
|
518
|
+
message: 'Current authentication method: default',
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Unified Login/Register endpoint
|
|
525
|
+
* Supports both OTP and default (email/password) authentication methods
|
|
526
|
+
* Automatically detects which method to use based on request body
|
|
527
|
+
*
|
|
528
|
+
* For OTP method:
|
|
529
|
+
* - First call: Send email/mobile to receive OTP (step: 'request')
|
|
530
|
+
* - Second call: Verify OTP to complete login (step: 'verify')
|
|
531
|
+
*
|
|
532
|
+
* For Default method:
|
|
533
|
+
* - Single call: Send email/username and password to login
|
|
534
|
+
* - Register: Send username, email, and password to register
|
|
535
|
+
*/
|
|
536
|
+
async unifiedAuth(ctx) {
|
|
537
|
+
try {
|
|
538
|
+
// Check ecommerce permission first
|
|
539
|
+
const hasPermission = await ensureEcommercePermission(ctx);
|
|
540
|
+
if (!hasPermission) {
|
|
541
|
+
return; // ensureEcommercePermission already sent the response
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Validate request body exists
|
|
545
|
+
if (!ctx.request.body || typeof ctx.request.body !== 'object') {
|
|
546
|
+
return ctx.badRequest('Request body is required. Please provide authentication credentials.');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const {
|
|
550
|
+
step, // 'request' or 'verify' for OTP, 'login' or 'register' for default
|
|
551
|
+
authMethod, // 'otp' or 'default' (optional, auto-detected if not provided)
|
|
552
|
+
// OTP fields
|
|
553
|
+
email,
|
|
554
|
+
mobile,
|
|
555
|
+
type, // 'email' or 'mobile' for OTP
|
|
556
|
+
otp, // OTP code for verification
|
|
557
|
+
// Default fields
|
|
558
|
+
identifier, // email or username for default login
|
|
559
|
+
password, // password for default login
|
|
560
|
+
username, // username for default register
|
|
561
|
+
} = ctx.request.body;
|
|
562
|
+
|
|
563
|
+
// Log request for debugging
|
|
564
|
+
strapi.log.debug(`[${PLUGIN_ID}] Unified auth request:`, {
|
|
565
|
+
step,
|
|
566
|
+
authMethod,
|
|
567
|
+
hasEmail: !!email,
|
|
568
|
+
hasMobile: !!mobile,
|
|
569
|
+
hasType: !!type,
|
|
570
|
+
hasOtp: !!otp,
|
|
571
|
+
hasIdentifier: !!identifier,
|
|
572
|
+
hasPassword: !!password,
|
|
573
|
+
hasUsername: !!username,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Get configured method
|
|
577
|
+
const configuredMethod = await getLoginMethod();
|
|
578
|
+
|
|
579
|
+
// Auto-detect authentication method if not provided
|
|
580
|
+
let detectedMethod = authMethod;
|
|
581
|
+
if (!detectedMethod) {
|
|
582
|
+
// If OTP fields are present, use OTP method
|
|
583
|
+
if ((email || mobile) && type) {
|
|
584
|
+
detectedMethod = 'otp';
|
|
585
|
+
}
|
|
586
|
+
// If identifier/password or username/email/password are present, use default
|
|
587
|
+
else if ((identifier && password) || (username && email && password)) {
|
|
588
|
+
detectedMethod = 'default';
|
|
589
|
+
}
|
|
590
|
+
// Default to configured method
|
|
591
|
+
else {
|
|
592
|
+
detectedMethod = configuredMethod;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// If configured method is 'both', allow both OTP and default
|
|
597
|
+
// Otherwise, validate that detected method matches configured method
|
|
598
|
+
if (configuredMethod !== 'both') {
|
|
599
|
+
if (detectedMethod === 'otp' && configuredMethod !== 'otp') {
|
|
600
|
+
return ctx.badRequest('OTP authentication is not enabled. Please enable it in plugin settings or use the unified endpoint.');
|
|
601
|
+
}
|
|
602
|
+
if (detectedMethod === 'default' && configuredMethod !== 'default') {
|
|
603
|
+
return ctx.badRequest('Default authentication is not enabled. Please enable it in plugin settings or use the unified endpoint.');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Handle OTP authentication
|
|
608
|
+
if (detectedMethod === 'otp') {
|
|
609
|
+
// Step 1: Request OTP
|
|
610
|
+
if (step === 'request' || (!step && !otp)) {
|
|
611
|
+
// Normalize email for case-insensitive match
|
|
612
|
+
let normalizedEmail = email?.toLowerCase();
|
|
613
|
+
|
|
614
|
+
if (!type || (type !== 'email' && type !== 'mobile')) {
|
|
615
|
+
return ctx.badRequest('Type must be "email" or "mobile" for OTP authentication.');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
let identifier = type === 'email' ? normalizedEmail : mobile;
|
|
619
|
+
|
|
620
|
+
if (!identifier) {
|
|
621
|
+
return ctx.badRequest(
|
|
622
|
+
`${type === 'email' ? 'Email' : 'Mobile number'} is required.`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Find user
|
|
627
|
+
let user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
628
|
+
where: type === 'email' ? { email: normalizedEmail } : { phone_no: mobile },
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
let isNewUser = false;
|
|
632
|
+
|
|
633
|
+
// Create user if not exists
|
|
634
|
+
if (!user) {
|
|
635
|
+
let generatedUsername = '';
|
|
636
|
+
|
|
637
|
+
if (type === 'email' && email) {
|
|
638
|
+
// EMAIL USERNAME — exactly 8 chars (letters + numbers)
|
|
639
|
+
let base = email.split('@')[0].replace(/[^a-zA-Z]/g, '').toLowerCase();
|
|
640
|
+
if (!base) base = 'user';
|
|
641
|
+
|
|
642
|
+
base = base.substring(0, 4); // max 4 letters
|
|
643
|
+
|
|
644
|
+
const digitsNeeded = 8 - base.length;
|
|
645
|
+
const min = Math.pow(10, digitsNeeded - 1);
|
|
646
|
+
const max = Math.pow(10, digitsNeeded) - 1;
|
|
647
|
+
|
|
648
|
+
let randomDigits = String(Math.floor(Math.random() * (max - min + 1)) + min);
|
|
649
|
+
|
|
650
|
+
generatedUsername = (base + randomDigits).substring(0, 8);
|
|
651
|
+
} else if (type === 'mobile' && mobile) {
|
|
652
|
+
// MOBILE USERNAME — MUST start with "webby" and be exactly 8 chars
|
|
653
|
+
const prefix = 'webby'; // length 5
|
|
654
|
+
const randomDigits = String(Math.floor(100 + Math.random() * 900)); // 100–999
|
|
655
|
+
generatedUsername = prefix + randomDigits; // total = 8 chars
|
|
656
|
+
} else {
|
|
657
|
+
generatedUsername = 'user' + String(Math.floor(1000 + Math.random() * 9000)); // fallback 8 chars
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Ensure unique
|
|
661
|
+
const existing = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
662
|
+
where: { username: generatedUsername },
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
if (existing) {
|
|
666
|
+
if (type === 'mobile') {
|
|
667
|
+
generatedUsername = 'webby' + String(Math.floor(100 + Math.random() * 900));
|
|
668
|
+
} else {
|
|
669
|
+
generatedUsername = generatedUsername.substring(0, 4) + String(Math.floor(1000 + Math.random() * 9000));
|
|
670
|
+
generatedUsername = generatedUsername.substring(0, 8);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Get default role
|
|
675
|
+
const defaultRole = await strapi.db
|
|
676
|
+
.query('plugin::users-permissions.role')
|
|
677
|
+
.findOne({ where: { type: 'public' } });
|
|
678
|
+
|
|
679
|
+
// Create user
|
|
680
|
+
user = await strapi.plugin('users-permissions').service('user').add({
|
|
681
|
+
email: type === 'email' ? normalizedEmail : null,
|
|
682
|
+
phone_no: type === 'mobile' ? mobile : null,
|
|
683
|
+
username: generatedUsername,
|
|
684
|
+
confirmed: false,
|
|
685
|
+
role: defaultRole?.id || 2,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
isNewUser = true;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Generate OTP (6 digits)
|
|
692
|
+
const otpCode = Math.floor(100000 + Math.random() * 900000);
|
|
693
|
+
const otpDigits = otpCode.toString().split('');
|
|
694
|
+
|
|
695
|
+
// Save OTP using raw query
|
|
696
|
+
try {
|
|
697
|
+
const db = strapi.db;
|
|
698
|
+
const knex = db.connection;
|
|
699
|
+
const tableName = 'up_users';
|
|
700
|
+
const client = db.config.connection.client;
|
|
701
|
+
|
|
702
|
+
if (client === 'postgres') {
|
|
703
|
+
await knex.raw(
|
|
704
|
+
`UPDATE ${tableName} SET otp = ?, is_otp_verified = ? WHERE id = ?`,
|
|
705
|
+
[otpCode, false, user.id]
|
|
706
|
+
);
|
|
707
|
+
} else if (client === 'mysql' || client === 'mysql2') {
|
|
708
|
+
await knex.raw(
|
|
709
|
+
`UPDATE \`${tableName}\` SET \`otp\` = ?, \`is_otp_verified\` = ? WHERE \`id\` = ?`,
|
|
710
|
+
[otpCode, false, user.id]
|
|
711
|
+
);
|
|
712
|
+
} else {
|
|
713
|
+
await knex.raw(
|
|
714
|
+
`UPDATE ${tableName} SET otp = ?, is_otp_verified = ? WHERE id = ?`,
|
|
715
|
+
[otpCode, false, user.id]
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
} catch (err) {
|
|
719
|
+
const { extendUserSchemaWithOtpFields } = require('../utils/extend-user-schema');
|
|
720
|
+
await extendUserSchemaWithOtpFields(strapi);
|
|
721
|
+
|
|
722
|
+
const db = strapi.db;
|
|
723
|
+
const knex = db.connection;
|
|
724
|
+
const tableName = 'up_users';
|
|
725
|
+
const client = db.config.connection.client;
|
|
726
|
+
|
|
727
|
+
if (client === 'postgres') {
|
|
728
|
+
await knex.raw(
|
|
729
|
+
`UPDATE ${tableName} SET otp = ?, is_otp_verified = ? WHERE id = ?`,
|
|
730
|
+
[otpCode, false, user.id]
|
|
731
|
+
);
|
|
732
|
+
} else if (client === 'mysql' || client === 'mysql2') {
|
|
733
|
+
await knex.raw(
|
|
734
|
+
`UPDATE \`${tableName}\` SET \`otp\` = ?, \`is_otp_verified\` = ? WHERE \`id\` = ?`,
|
|
735
|
+
[otpCode, false, user.id]
|
|
736
|
+
);
|
|
737
|
+
} else {
|
|
738
|
+
await knex.raw(
|
|
739
|
+
`UPDATE ${tableName} SET otp = ?, is_otp_verified = ? WHERE id = ?`,
|
|
740
|
+
[otpCode, false, user.id]
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Send OTP via email
|
|
746
|
+
let emailSent = false;
|
|
747
|
+
if (type === 'email') {
|
|
748
|
+
const otpEmailHTML = `
|
|
749
|
+
<!DOCTYPE html>
|
|
750
|
+
<html lang="en">
|
|
751
|
+
<head>
|
|
752
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
753
|
+
<meta charset="UTF-8" />
|
|
754
|
+
<title>OTP Confirmation</title>
|
|
755
|
+
</head>
|
|
756
|
+
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: Arial, sans-serif;">
|
|
757
|
+
<table align="center" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width: 650px; margin: 0 auto; background-color: #ffffff;">
|
|
758
|
+
<tr>
|
|
759
|
+
<td align="center" style="padding: 60px 20px 8px;">
|
|
760
|
+
<h1 style="font-weight: bold; color: #111; font-size: 32px; margin: 0;">Your OTP Code for Secure Access</h1>
|
|
761
|
+
</td>
|
|
762
|
+
</tr>
|
|
763
|
+
<tr>
|
|
764
|
+
<td align="center" style="padding: 16px 30px;">
|
|
765
|
+
<p style="font-size: 16px; color: #333333; line-height: 24px; margin: 0; max-width: 500px;">
|
|
766
|
+
To complete your account verification, please use the One-Time Password (OTP) provided below. For security reasons, this OTP will expire in 10 minutes and can only be used once.
|
|
767
|
+
</p>
|
|
768
|
+
</td>
|
|
769
|
+
</tr>
|
|
770
|
+
<tr>
|
|
771
|
+
<td align="center" style="padding: 30px 0;">
|
|
772
|
+
<table cellpadding="0" cellspacing="0" border="0" style="margin: 0 auto;">
|
|
773
|
+
<tr>
|
|
774
|
+
<td style="padding: 0 6px;">
|
|
775
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
776
|
+
<tr>
|
|
777
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[0]}</td>
|
|
778
|
+
</tr>
|
|
779
|
+
</table>
|
|
780
|
+
</td>
|
|
781
|
+
<td style="padding: 0 6px;">
|
|
782
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
783
|
+
<tr>
|
|
784
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[1]}</td>
|
|
785
|
+
</tr>
|
|
786
|
+
</table>
|
|
787
|
+
</td>
|
|
788
|
+
<td style="padding: 0 6px;">
|
|
789
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
790
|
+
<tr>
|
|
791
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[2]}</td>
|
|
792
|
+
</tr>
|
|
793
|
+
</table>
|
|
794
|
+
</td>
|
|
795
|
+
<td style="padding: 0 6px;">
|
|
796
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
797
|
+
<tr>
|
|
798
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[3]}</td>
|
|
799
|
+
</tr>
|
|
800
|
+
</table>
|
|
801
|
+
</td>
|
|
802
|
+
<td style="padding: 0 6px;">
|
|
803
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
804
|
+
<tr>
|
|
805
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[4]}</td>
|
|
806
|
+
</tr>
|
|
807
|
+
</table>
|
|
808
|
+
</td>
|
|
809
|
+
<td style="padding: 0 6px;">
|
|
810
|
+
<table cellpadding="0" cellspacing="0" border="0" style="width: 60px; height: 60px; border: 1px solid #0156D559; border-radius: 8px; background-color: #f8f9fa;">
|
|
811
|
+
<tr>
|
|
812
|
+
<td align="center" style="font-size: 28px; font-weight: bold; color: #111;">${otpDigits[5]}</td>
|
|
813
|
+
</tr>
|
|
814
|
+
</table>
|
|
815
|
+
</td>
|
|
816
|
+
</tr>
|
|
817
|
+
</table>
|
|
818
|
+
</td>
|
|
819
|
+
</tr>
|
|
820
|
+
</table>
|
|
821
|
+
</body>
|
|
822
|
+
</html>
|
|
823
|
+
`;
|
|
824
|
+
|
|
825
|
+
try {
|
|
826
|
+
await sendEmail({
|
|
827
|
+
to: email,
|
|
828
|
+
subject: 'Your OTP Code - Strapi WebbyCommerce',
|
|
829
|
+
html: otpEmailHTML,
|
|
830
|
+
});
|
|
831
|
+
emailSent = true;
|
|
832
|
+
} catch (emailError) {
|
|
833
|
+
strapi.log.error(
|
|
834
|
+
`[${PLUGIN_ID}] Failed to send OTP email (userId: ${user.id}, email: ${email}):`,
|
|
835
|
+
emailError
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
} else if (type === 'mobile') {
|
|
839
|
+
strapi.log.info(`[${PLUGIN_ID}] OTP for mobile ${mobile}: ${otpCode}`);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
ctx.send({
|
|
843
|
+
success: true,
|
|
844
|
+
step: 'request',
|
|
845
|
+
method: 'otp',
|
|
846
|
+
message: emailSent
|
|
847
|
+
? `OTP sent to ${type}.`
|
|
848
|
+
: type === 'email'
|
|
849
|
+
? 'User created. OTP email could not be sent; please check email configuration on the server.'
|
|
850
|
+
: `OTP sent to ${type}.`,
|
|
851
|
+
userId: user.id,
|
|
852
|
+
isNewUser,
|
|
853
|
+
emailSent,
|
|
854
|
+
nextStep: 'verify',
|
|
855
|
+
});
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Step 2: Verify OTP
|
|
860
|
+
if (step === 'verify' || (!step && otp)) {
|
|
861
|
+
if (!otp || !((type === 'email' && email) || (type === 'mobile' && mobile))) {
|
|
862
|
+
return ctx.badRequest(
|
|
863
|
+
`${type === 'email' ? 'Email' : 'Mobile number'} and OTP are required.`
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const identifier = type === 'email' ? email.toLowerCase() : mobile;
|
|
868
|
+
|
|
869
|
+
// Get user
|
|
870
|
+
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
871
|
+
where: { [type === 'email' ? 'email' : 'phone_no']: identifier },
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
if (!user) return ctx.badRequest('User not found.');
|
|
875
|
+
|
|
876
|
+
// Get OTP fields using raw query
|
|
877
|
+
const db = strapi.db;
|
|
878
|
+
const knex = db.connection;
|
|
879
|
+
const tableName = 'up_users';
|
|
880
|
+
const client = db.config.connection.client;
|
|
881
|
+
|
|
882
|
+
let userOtpData;
|
|
883
|
+
let columnsExist = false;
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
if (client === 'postgres') {
|
|
887
|
+
const result = await knex.raw(
|
|
888
|
+
`SELECT otp, is_otp_verified FROM ${tableName} WHERE id = ?`,
|
|
889
|
+
[user.id]
|
|
890
|
+
);
|
|
891
|
+
userOtpData = result.rows[0];
|
|
892
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
893
|
+
} else if (client === 'mysql' || client === 'mysql2') {
|
|
894
|
+
const result = await knex.raw(
|
|
895
|
+
`SELECT \`otp\`, \`is_otp_verified\` FROM \`${tableName}\` WHERE \`id\` = ?`,
|
|
896
|
+
[user.id]
|
|
897
|
+
);
|
|
898
|
+
userOtpData = result[0][0];
|
|
899
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
900
|
+
} else {
|
|
901
|
+
const result = await knex.raw(
|
|
902
|
+
`SELECT otp, is_otp_verified FROM ${tableName} WHERE id = ?`,
|
|
903
|
+
[user.id]
|
|
904
|
+
);
|
|
905
|
+
userOtpData = result[0];
|
|
906
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
907
|
+
}
|
|
908
|
+
} catch (queryErr) {
|
|
909
|
+
strapi.log.warn(`[${PLUGIN_ID}] OTP columns not found, attempting to add them:`, queryErr.message);
|
|
910
|
+
columnsExist = false;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (!columnsExist) {
|
|
914
|
+
const { extendUserSchemaWithOtpFields } = require('../utils/extend-user-schema');
|
|
915
|
+
await extendUserSchemaWithOtpFields(strapi);
|
|
916
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
917
|
+
|
|
918
|
+
try {
|
|
919
|
+
if (client === 'postgres') {
|
|
920
|
+
const result = await knex.raw(
|
|
921
|
+
`SELECT otp, is_otp_verified FROM ${tableName} WHERE id = ?`,
|
|
922
|
+
[user.id]
|
|
923
|
+
);
|
|
924
|
+
userOtpData = result.rows[0];
|
|
925
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
926
|
+
} else if (client === 'mysql' || client === 'mysql2') {
|
|
927
|
+
const result = await knex.raw(
|
|
928
|
+
`SELECT \`otp\`, \`is_otp_verified\` FROM \`${tableName}\` WHERE \`id\` = ?`,
|
|
929
|
+
[user.id]
|
|
930
|
+
);
|
|
931
|
+
userOtpData = result[0][0];
|
|
932
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
933
|
+
} else {
|
|
934
|
+
const result = await knex.raw(
|
|
935
|
+
`SELECT otp, is_otp_verified FROM ${tableName} WHERE id = ?`,
|
|
936
|
+
[user.id]
|
|
937
|
+
);
|
|
938
|
+
userOtpData = result[0];
|
|
939
|
+
columnsExist = userOtpData && userOtpData.hasOwnProperty('otp') && userOtpData.hasOwnProperty('is_otp_verified');
|
|
940
|
+
}
|
|
941
|
+
} catch (retryErr) {
|
|
942
|
+
return ctx.badRequest(
|
|
943
|
+
'OTP fields are not available. Please restart Strapi after extending the user schema.'
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const userOtp = userOtpData?.otp;
|
|
949
|
+
const isOtpVerified = userOtpData?.is_otp_verified;
|
|
950
|
+
|
|
951
|
+
if (isOtpVerified) return ctx.badRequest('User already verified.');
|
|
952
|
+
if (userOtp !== parseInt(otp, 10)) return ctx.badRequest('Invalid OTP.');
|
|
953
|
+
|
|
954
|
+
// Update user verification
|
|
955
|
+
try {
|
|
956
|
+
if (client === 'postgres') {
|
|
957
|
+
await knex.raw(
|
|
958
|
+
`UPDATE ${tableName} SET is_otp_verified = ?, confirmed = true, otp = NULL WHERE id = ?`,
|
|
959
|
+
[true, user.id]
|
|
960
|
+
);
|
|
961
|
+
} else if (client === 'mysql' || client === 'mysql2') {
|
|
962
|
+
await knex.raw(
|
|
963
|
+
`UPDATE \`${tableName}\` SET \`is_otp_verified\` = ?, \`confirmed\` = true, \`otp\` = NULL WHERE \`id\` = ?`,
|
|
964
|
+
[true, user.id]
|
|
965
|
+
);
|
|
966
|
+
} else {
|
|
967
|
+
await knex.raw(
|
|
968
|
+
`UPDATE ${tableName} SET is_otp_verified = ?, confirmed = 1, otp = NULL WHERE id = ?`,
|
|
969
|
+
[true, user.id]
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
} catch (dbErr) {
|
|
973
|
+
strapi.log.error(`[${PLUGIN_ID}] Database error during OTP verification:`, dbErr);
|
|
974
|
+
return ctx.badRequest(
|
|
975
|
+
'OTP fields are not available in the user schema. Please extend the user schema as described in the plugin README.'
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Generate JWT token
|
|
980
|
+
const jwt = strapi.plugins['users-permissions'].services.jwt.issue({
|
|
981
|
+
id: user.id,
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
ctx.send({
|
|
985
|
+
success: true,
|
|
986
|
+
step: 'verify',
|
|
987
|
+
method: 'otp',
|
|
988
|
+
message: 'Login successfully!',
|
|
989
|
+
jwt,
|
|
990
|
+
user: {
|
|
991
|
+
id: user.id,
|
|
992
|
+
username: user.username,
|
|
993
|
+
email: user.email,
|
|
994
|
+
phone_no: user.phone_no,
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return ctx.badRequest('Invalid step for OTP authentication. Use "request" or "verify".');
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Handle Default authentication
|
|
1004
|
+
if (detectedMethod === 'default') {
|
|
1005
|
+
// Login
|
|
1006
|
+
if (step === 'login' || (!step && identifier && password)) {
|
|
1007
|
+
if (!identifier || !password) {
|
|
1008
|
+
return ctx.badRequest('Identifier (email/username) and password are required for login.');
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Use Strapi's built-in authentication
|
|
1012
|
+
const userService = strapi.plugin('users-permissions').service('user');
|
|
1013
|
+
const jwt = strapi.plugins['users-permissions'].services.jwt;
|
|
1014
|
+
|
|
1015
|
+
// Find user by identifier (email or username)
|
|
1016
|
+
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
1017
|
+
where: {
|
|
1018
|
+
$or: [
|
|
1019
|
+
{ email: identifier.toLowerCase() },
|
|
1020
|
+
{ username: identifier },
|
|
1021
|
+
],
|
|
1022
|
+
},
|
|
1023
|
+
populate: ['role'],
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
if (!user) {
|
|
1027
|
+
return ctx.badRequest('Invalid identifier or password.');
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Validate password using bcrypt compare
|
|
1031
|
+
if (!user.password) {
|
|
1032
|
+
return ctx.badRequest('Invalid identifier or password.');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const validPassword = await bcrypt.compare(password, user.password);
|
|
1036
|
+
|
|
1037
|
+
if (!validPassword) {
|
|
1038
|
+
return ctx.badRequest('Invalid identifier or password.');
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Check if user is blocked
|
|
1042
|
+
if (user.blocked) {
|
|
1043
|
+
return ctx.badRequest('Your account has been blocked.');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Generate JWT token
|
|
1047
|
+
const token = jwt.issue({
|
|
1048
|
+
id: user.id,
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
ctx.send({
|
|
1052
|
+
success: true,
|
|
1053
|
+
step: 'login',
|
|
1054
|
+
method: 'default',
|
|
1055
|
+
message: 'Login successfully!',
|
|
1056
|
+
jwt: token,
|
|
1057
|
+
user: {
|
|
1058
|
+
id: user.id,
|
|
1059
|
+
username: user.username,
|
|
1060
|
+
email: user.email,
|
|
1061
|
+
phone_no: user.phone_no,
|
|
1062
|
+
role: user.role
|
|
1063
|
+
? {
|
|
1064
|
+
id: user.role.id,
|
|
1065
|
+
name: user.role.name,
|
|
1066
|
+
type: user.role.type,
|
|
1067
|
+
}
|
|
1068
|
+
: null,
|
|
1069
|
+
},
|
|
1070
|
+
});
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Register
|
|
1075
|
+
if (step === 'register' || (!step && username && email && password)) {
|
|
1076
|
+
if (!username || !email || !password) {
|
|
1077
|
+
return ctx.badRequest('Username, email, and password are required for registration.');
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Validate email format
|
|
1081
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1082
|
+
if (!emailRegex.test(email.trim())) {
|
|
1083
|
+
return ctx.badRequest('Invalid email format.');
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Check if email already exists
|
|
1087
|
+
const existingUser = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
1088
|
+
where: {
|
|
1089
|
+
$or: [
|
|
1090
|
+
{ email: email.toLowerCase() },
|
|
1091
|
+
{ username: username },
|
|
1092
|
+
],
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
if (existingUser) {
|
|
1097
|
+
return ctx.badRequest('Email or username already exists.');
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Get default role
|
|
1101
|
+
const defaultRole = await strapi.db
|
|
1102
|
+
.query('plugin::users-permissions.role')
|
|
1103
|
+
.findOne({ where: { type: 'public' } });
|
|
1104
|
+
|
|
1105
|
+
// Create user using Strapi's user service
|
|
1106
|
+
const user = await strapi.plugin('users-permissions').service('user').add({
|
|
1107
|
+
username: username.trim(),
|
|
1108
|
+
email: email.trim().toLowerCase(),
|
|
1109
|
+
password: password,
|
|
1110
|
+
confirmed: true, // Auto-confirm for default method
|
|
1111
|
+
role: defaultRole?.id || 2,
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// Generate JWT token
|
|
1115
|
+
const jwt = strapi.plugins['users-permissions'].services.jwt.issue({
|
|
1116
|
+
id: user.id,
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
ctx.send({
|
|
1120
|
+
success: true,
|
|
1121
|
+
step: 'register',
|
|
1122
|
+
method: 'default',
|
|
1123
|
+
message: 'Registration successful!',
|
|
1124
|
+
jwt,
|
|
1125
|
+
user: {
|
|
1126
|
+
id: user.id,
|
|
1127
|
+
username: user.username,
|
|
1128
|
+
email: user.email,
|
|
1129
|
+
role: user.role
|
|
1130
|
+
? {
|
|
1131
|
+
id: user.role.id,
|
|
1132
|
+
name: user.role.name,
|
|
1133
|
+
type: user.role.type,
|
|
1134
|
+
}
|
|
1135
|
+
: null,
|
|
1136
|
+
},
|
|
1137
|
+
});
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return ctx.badRequest('Invalid step for default authentication. Use "login" or "register".');
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// If we reach here, no valid authentication method was detected
|
|
1145
|
+
return ctx.badRequest(
|
|
1146
|
+
'Invalid request. Please provide valid authentication credentials. ' +
|
|
1147
|
+
'For OTP: provide step="request" with email/mobile and type, or step="verify" with OTP code. ' +
|
|
1148
|
+
'For default: provide step="login" with identifier and password, or step="register" with username, email, and password.'
|
|
1149
|
+
);
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in unifiedAuth:`, error);
|
|
1152
|
+
strapi.log.error(`[${PLUGIN_ID}] Error stack:`, error.stack);
|
|
1153
|
+
strapi.log.error(`[${PLUGIN_ID}] Request body:`, JSON.stringify(ctx.request.body, null, 2));
|
|
1154
|
+
|
|
1155
|
+
// Provide more specific error messages
|
|
1156
|
+
if (error.message) {
|
|
1157
|
+
return ctx.internalServerError(`Authentication failed: ${error.message}`);
|
|
1158
|
+
}
|
|
1159
|
+
ctx.internalServerError('Authentication failed. Please try again.');
|
|
1160
|
+
}
|
|
1161
|
+
},
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Get authenticated user profile
|
|
1165
|
+
* Returns all user details for the authenticated user
|
|
1166
|
+
*/
|
|
1167
|
+
async getProfile(ctx) {
|
|
1168
|
+
try {
|
|
1169
|
+
// Check ecommerce permission
|
|
1170
|
+
const hasPermission = await ensureEcommercePermission(ctx);
|
|
1171
|
+
if (!hasPermission) {
|
|
1172
|
+
return; // ensureEcommercePermission already sent the response
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Get authenticated user from context
|
|
1176
|
+
const user = ctx.state.user;
|
|
1177
|
+
|
|
1178
|
+
if (!user) {
|
|
1179
|
+
return ctx.unauthorized('Authentication required. Please provide a valid JWT token.');
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Fetch full user details from database
|
|
1183
|
+
const fullUser = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
1184
|
+
where: { id: user.id },
|
|
1185
|
+
populate: ['role'],
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
if (!fullUser) {
|
|
1189
|
+
return ctx.notFound('User not found.');
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Return user profile (exclude sensitive/private fields)
|
|
1193
|
+
// Only include fields that exist in the user schema
|
|
1194
|
+
ctx.send({
|
|
1195
|
+
user: {
|
|
1196
|
+
id: fullUser.id,
|
|
1197
|
+
username: fullUser.username ?? null,
|
|
1198
|
+
email: fullUser.email ?? null,
|
|
1199
|
+
phone_no: fullUser.phone_no ?? null,
|
|
1200
|
+
first_name: fullUser.first_name ?? null,
|
|
1201
|
+
last_name: fullUser.last_name ?? null,
|
|
1202
|
+
display_name: fullUser.display_name ?? null,
|
|
1203
|
+
company_name: fullUser.company_name ?? null,
|
|
1204
|
+
confirmed: fullUser.confirmed ?? false,
|
|
1205
|
+
blocked: fullUser.blocked ?? false,
|
|
1206
|
+
role: fullUser.role
|
|
1207
|
+
? {
|
|
1208
|
+
id: fullUser.role.id,
|
|
1209
|
+
name: fullUser.role.name,
|
|
1210
|
+
type: fullUser.role.type,
|
|
1211
|
+
}
|
|
1212
|
+
: null,
|
|
1213
|
+
createdAt: fullUser.createdAt ?? null,
|
|
1214
|
+
updatedAt: fullUser.updatedAt ?? null,
|
|
1215
|
+
// Only include fields that exist in the schema
|
|
1216
|
+
// Excluded: password, resetPasswordToken, confirmationToken (private fields)
|
|
1217
|
+
// Excluded: provider, otp, isOtpVerified (internal fields, not needed in profile)
|
|
1218
|
+
},
|
|
1219
|
+
});
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getProfile:`, error);
|
|
1222
|
+
ctx.internalServerError('Failed to fetch user profile. Please try again.');
|
|
1223
|
+
}
|
|
1224
|
+
},
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Update user profile
|
|
1228
|
+
* Updates user details including first_name, last_name, email, phone_no, and optional display name
|
|
1229
|
+
* Also supports password update if default login method is enabled
|
|
1230
|
+
*/
|
|
1231
|
+
async updateProfile(ctx) {
|
|
1232
|
+
try {
|
|
1233
|
+
// Check ecommerce permission
|
|
1234
|
+
const hasPermission = await ensureEcommercePermission(ctx);
|
|
1235
|
+
if (!hasPermission) {
|
|
1236
|
+
return; // ensureEcommercePermission already sent the response
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Get authenticated user from context
|
|
1240
|
+
const user = ctx.state.user;
|
|
1241
|
+
|
|
1242
|
+
if (!user) {
|
|
1243
|
+
return ctx.unauthorized('Authentication required. Please provide a valid JWT token.');
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const {
|
|
1247
|
+
first_name,
|
|
1248
|
+
last_name,
|
|
1249
|
+
email,
|
|
1250
|
+
phone_no,
|
|
1251
|
+
display_name,
|
|
1252
|
+
company_name,
|
|
1253
|
+
currentPassword,
|
|
1254
|
+
newPassword,
|
|
1255
|
+
} = ctx.request.body;
|
|
1256
|
+
|
|
1257
|
+
// Validate required fields
|
|
1258
|
+
if (!first_name || typeof first_name !== 'string' || first_name.trim().length === 0) {
|
|
1259
|
+
return ctx.badRequest('First name is required.');
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (!last_name || typeof last_name !== 'string' || last_name.trim().length === 0) {
|
|
1263
|
+
return ctx.badRequest('Last name is required.');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (!email || typeof email !== 'string' || email.trim().length === 0) {
|
|
1267
|
+
return ctx.badRequest('Email address is required.');
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Validate email format
|
|
1271
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1272
|
+
if (!emailRegex.test(email.trim())) {
|
|
1273
|
+
return ctx.badRequest('Invalid email format.');
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (!phone_no || typeof phone_no !== 'string' || phone_no.trim().length === 0) {
|
|
1277
|
+
return ctx.badRequest('Phone number is required.');
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Check if email is unique (excluding current user)
|
|
1281
|
+
const existingUserByEmail = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
1282
|
+
where: {
|
|
1283
|
+
email: email.trim().toLowerCase(),
|
|
1284
|
+
$not: { id: user.id },
|
|
1285
|
+
},
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
if (existingUserByEmail) {
|
|
1289
|
+
return ctx.badRequest('Email address is already in use by another user.');
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Check if phone_no is unique (excluding current user)
|
|
1293
|
+
const existingUserByPhone = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
1294
|
+
where: {
|
|
1295
|
+
phone_no: phone_no.trim(),
|
|
1296
|
+
$not: { id: user.id },
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
if (existingUserByPhone) {
|
|
1301
|
+
return ctx.badRequest('Phone number is already in use by another user.');
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Prepare update data
|
|
1305
|
+
const updateData = {
|
|
1306
|
+
first_name: first_name.trim(),
|
|
1307
|
+
last_name: last_name.trim(),
|
|
1308
|
+
email: email.trim().toLowerCase(),
|
|
1309
|
+
phone_no: phone_no.trim(),
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
// Add display_name if provided (optional)
|
|
1313
|
+
if (display_name !== undefined) {
|
|
1314
|
+
updateData.display_name = display_name.trim() || null;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Add company_name if provided (optional)
|
|
1318
|
+
if (company_name !== undefined) {
|
|
1319
|
+
updateData.company_name = company_name.trim() || null;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Handle password update (only if default login method is enabled)
|
|
1323
|
+
if (currentPassword || newPassword) {
|
|
1324
|
+
const method = await getLoginMethod();
|
|
1325
|
+
if (method !== 'default') {
|
|
1326
|
+
return ctx.badRequest(
|
|
1327
|
+
'Password update is only available when using the default email/password authentication method.'
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (!currentPassword || !newPassword) {
|
|
1332
|
+
return ctx.badRequest('Both current password and new password are required for password update.');
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// Verify current password
|
|
1336
|
+
const currentUser = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
1337
|
+
where: { id: user.id },
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
if (!currentUser) {
|
|
1341
|
+
return ctx.notFound('User not found.');
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (!currentUser.password) {
|
|
1345
|
+
return ctx.badRequest('Current password is incorrect.');
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const validPassword = await bcrypt.compare(currentPassword, currentUser.password);
|
|
1349
|
+
|
|
1350
|
+
if (!validPassword) {
|
|
1351
|
+
return ctx.badRequest('Current password is incorrect.');
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Validate new password length
|
|
1355
|
+
if (newPassword.length < 6) {
|
|
1356
|
+
return ctx.badRequest('New password must be at least 6 characters long.');
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Hash and set new password
|
|
1360
|
+
const hashedPassword = await strapi
|
|
1361
|
+
.plugin('users-permissions')
|
|
1362
|
+
.service('users-permissions')
|
|
1363
|
+
.hashPassword(newPassword);
|
|
1364
|
+
|
|
1365
|
+
updateData.password = hashedPassword;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Update user in database
|
|
1369
|
+
const updatedUser = await strapi.db.query('plugin::users-permissions.user').update({
|
|
1370
|
+
where: { id: user.id },
|
|
1371
|
+
data: updateData,
|
|
1372
|
+
populate: ['role'],
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
if (!updatedUser) {
|
|
1376
|
+
return ctx.notFound('User not found.');
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// Return updated user profile (exclude sensitive fields)
|
|
1380
|
+
ctx.send({
|
|
1381
|
+
message: 'Profile updated successfully.',
|
|
1382
|
+
user: {
|
|
1383
|
+
id: updatedUser.id,
|
|
1384
|
+
username: updatedUser.username,
|
|
1385
|
+
email: updatedUser.email,
|
|
1386
|
+
phone_no: updatedUser.phone_no,
|
|
1387
|
+
first_name: updatedUser.first_name,
|
|
1388
|
+
last_name: updatedUser.last_name,
|
|
1389
|
+
display_name: updatedUser.display_name || null,
|
|
1390
|
+
company_name: updatedUser.company_name || null,
|
|
1391
|
+
confirmed: updatedUser.confirmed,
|
|
1392
|
+
blocked: updatedUser.blocked,
|
|
1393
|
+
role: updatedUser.role
|
|
1394
|
+
? {
|
|
1395
|
+
id: updatedUser.role.id,
|
|
1396
|
+
name: updatedUser.role.name,
|
|
1397
|
+
type: updatedUser.role.type,
|
|
1398
|
+
}
|
|
1399
|
+
: null,
|
|
1400
|
+
updatedAt: updatedUser.updatedAt,
|
|
1401
|
+
},
|
|
1402
|
+
});
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in updateProfile:`, error);
|
|
1405
|
+
ctx.internalServerError('Failed to update profile. Please try again.');
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1408
|
+
};
|
|
1409
|
+
|