@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.
Files changed (162) hide show
  1. package/README.md +26 -3
  2. package/admin/app.js +3 -0
  3. package/admin/jsconfig.json +20 -0
  4. package/admin/src/components/ApiCollectionsContent.jsx +4626 -0
  5. package/admin/src/components/CompareContent.jsx +300 -0
  6. package/admin/src/components/ConfigureContent.jsx +407 -0
  7. package/admin/src/components/Initializer.jsx +64 -0
  8. package/admin/src/components/LoginRegisterContent.jsx +280 -0
  9. package/admin/src/components/PluginIcon.jsx +6 -0
  10. package/admin/src/components/ShippingTypeContent.jsx +230 -0
  11. package/admin/src/components/SmtpContent.jsx +316 -0
  12. package/admin/src/components/WishlistContent.jsx +273 -0
  13. package/admin/src/index.js +81 -0
  14. package/admin/src/pages/ApiCollections.jsx +169 -0
  15. package/admin/src/pages/Configure.jsx +55 -0
  16. package/admin/src/pages/Settings.jsx +93 -0
  17. package/admin/src/pluginId.js +4 -0
  18. package/{dist/_chunks/en-CiQ97iC8.js → admin/src/translations/en.json} +712 -574
  19. package/bin/setup.js +50 -3
  20. package/package.json +14 -13
  21. package/server/bootstrap.js +3 -0
  22. package/server/register.js +3 -0
  23. package/server/src/bootstrap.js +3826 -0
  24. package/server/src/components/content-block.json +37 -0
  25. package/server/src/components/shipping-zone-location.json +27 -0
  26. package/server/src/config/index.js +7 -0
  27. package/server/src/content-types/address/index.js +7 -0
  28. package/server/src/content-types/address/schema.json +74 -0
  29. package/server/src/content-types/cart/index.js +61 -0
  30. package/server/src/content-types/cart-item/index.js +79 -0
  31. package/server/src/content-types/compare.js +73 -0
  32. package/server/src/content-types/coupon/index.js +7 -0
  33. package/server/src/content-types/coupon/schema.json +67 -0
  34. package/server/src/content-types/index.js +42 -0
  35. package/server/src/content-types/order/index.js +7 -0
  36. package/server/src/content-types/order/schema.json +121 -0
  37. package/server/src/content-types/payment-transaction/index.js +7 -0
  38. package/server/src/content-types/payment-transaction/schema.json +73 -0
  39. package/server/src/content-types/product/index.js +7 -0
  40. package/server/src/content-types/product/schema.json +104 -0
  41. package/server/src/content-types/product-attribute/index.js +7 -0
  42. package/server/src/content-types/product-attribute/schema.json +80 -0
  43. package/server/src/content-types/product-attribute-value/index.js +7 -0
  44. package/server/src/content-types/product-attribute-value/schema.json +52 -0
  45. package/server/src/content-types/product-category/index.js +7 -0
  46. package/server/src/content-types/product-category/schema.json +54 -0
  47. package/server/src/content-types/product-tag/index.js +7 -0
  48. package/server/src/content-types/product-tag/schema.json +38 -0
  49. package/server/src/content-types/product-variation/index.js +7 -0
  50. package/server/src/content-types/product-variation/schema.json +74 -0
  51. package/server/src/content-types/shipping-method/index.js +7 -0
  52. package/server/src/content-types/shipping-method/schema.json +91 -0
  53. package/server/src/content-types/shipping-rate/index.js +7 -0
  54. package/server/src/content-types/shipping-rate/schema.json +73 -0
  55. package/server/src/content-types/shipping-rule/index.js +7 -0
  56. package/server/src/content-types/shipping-rule/schema.json +84 -0
  57. package/server/src/content-types/shipping-zone/index.js +7 -0
  58. package/server/src/content-types/shipping-zone/schema.json +57 -0
  59. package/server/src/content-types/wishlist.js +66 -0
  60. package/server/src/controllers/address.js +374 -0
  61. package/server/src/controllers/auth.js +1409 -0
  62. package/server/src/controllers/cart.js +337 -0
  63. package/server/src/controllers/category.js +388 -0
  64. package/server/src/controllers/compare.js +246 -0
  65. package/server/src/controllers/controller.js +168 -0
  66. package/server/src/controllers/ecommerce.js +20 -0
  67. package/server/src/controllers/index.js +34 -0
  68. package/server/src/controllers/order.js +1100 -0
  69. package/server/src/controllers/payment.js +243 -0
  70. package/server/src/controllers/product.js +1006 -0
  71. package/server/src/controllers/productTag.js +370 -0
  72. package/server/src/controllers/productVariation.js +181 -0
  73. package/server/src/controllers/shipping.js +1046 -0
  74. package/server/src/controllers/wishlist.js +332 -0
  75. package/server/src/destroy.js +6 -0
  76. package/server/src/index.js +26 -0
  77. package/server/src/middlewares/index.js +4 -0
  78. package/server/src/policies/index.js +4 -0
  79. package/server/src/register.js +67 -0
  80. package/server/src/routes/index.js +1130 -0
  81. package/server/src/services/cart.js +531 -0
  82. package/server/src/services/compare.js +300 -0
  83. package/server/src/services/index.js +16 -0
  84. package/server/src/services/service.js +19 -0
  85. package/server/src/services/shipping.js +513 -0
  86. package/server/src/services/wishlist.js +238 -0
  87. package/server/src/utils/check-ecommerce-permission.js +204 -0
  88. package/server/src/utils/extend-user-schema.js +161 -0
  89. package/server/src/utils/seed-data.js +639 -0
  90. package/server/src/utils/send-email.js +98 -0
  91. package/strapi-server.js +1 -6
  92. package/dist/_chunks/Settings-DZXAkI24.js +0 -31539
  93. package/dist/_chunks/Settings-yLx-YvVy.mjs +0 -31520
  94. package/dist/_chunks/en-DE15m4xZ.mjs +0 -574
  95. package/dist/_chunks/index-CXGrFKp6.mjs +0 -128
  96. package/dist/_chunks/index-DgocXUgC.js +0 -127
  97. package/dist/admin/index.js +0 -3
  98. package/dist/admin/index.mjs +0 -4
  99. package/dist/robots.txt +0 -3
  100. package/dist/server/index.js +0 -27078
  101. package/dist/uploads/.gitkeep +0 -0
  102. package/dist/uploads/accessories_category_2a5631094b.jpeg +0 -0
  103. package/dist/uploads/beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
  104. package/dist/uploads/books_category_a9a253eada.jpeg +0 -0
  105. package/dist/uploads/classic_cotton_tshirt_1_cd713425f6.png +0 -0
  106. package/dist/uploads/clothing_category_d5c60ef07b.jpeg +0 -0
  107. package/dist/uploads/daviddoe_strapi_adbcd41787.jpeg +0 -0
  108. package/dist/uploads/electronics_category_fc3e5ef571.jpeg +0 -0
  109. package/dist/uploads/ergonomic_office_chair_1_c751cffb07.png +0 -0
  110. package/dist/uploads/home_garden_category_4f6eb3f8d6.jpeg +0 -0
  111. package/dist/uploads/istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
  112. package/dist/uploads/istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
  113. package/dist/uploads/large_daviddoe_strapi_adbcd41787.jpeg +0 -0
  114. package/dist/uploads/leather_travel_backpack_1_238bc1ae4d.png +0 -0
  115. package/dist/uploads/mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  116. package/dist/uploads/medium_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  117. package/dist/uploads/medium_daviddoe_strapi_adbcd41787.jpeg +0 -0
  118. package/dist/uploads/medium_ergonomic_office_chair_1_c751cffb07.png +0 -0
  119. package/dist/uploads/medium_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  120. package/dist/uploads/medium_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  121. package/dist/uploads/medium_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  122. package/dist/uploads/medium_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  123. package/dist/uploads/medium_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  124. package/dist/uploads/medium_wireless_headphones_1_fa75cd50c3.png +0 -0
  125. package/dist/uploads/medium_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  126. package/dist/uploads/predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
  127. package/dist/uploads/small_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  128. package/dist/uploads/small_daviddoe_strapi_adbcd41787.jpeg +0 -0
  129. package/dist/uploads/small_ergonomic_office_chair_1_c751cffb07.png +0 -0
  130. package/dist/uploads/small_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  131. package/dist/uploads/small_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  132. package/dist/uploads/small_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  133. package/dist/uploads/small_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  134. package/dist/uploads/small_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  135. package/dist/uploads/small_wireless_headphones_1_fa75cd50c3.png +0 -0
  136. package/dist/uploads/small_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  137. package/dist/uploads/smart_watch_series_5_1_cdc2511fb7.png +0 -0
  138. package/dist/uploads/smartphone_x_pro_1_c3f0cbd080.png +0 -0
  139. package/dist/uploads/the_great_gatsby_special_1_2e7c76d997.png +0 -0
  140. package/dist/uploads/thumbnail_accessories_category_2a5631094b.jpeg +0 -0
  141. package/dist/uploads/thumbnail_beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
  142. package/dist/uploads/thumbnail_books_category_a9a253eada.jpeg +0 -0
  143. package/dist/uploads/thumbnail_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  144. package/dist/uploads/thumbnail_clothing_category_d5c60ef07b.jpeg +0 -0
  145. package/dist/uploads/thumbnail_daviddoe_strapi_adbcd41787.jpeg +0 -0
  146. package/dist/uploads/thumbnail_electronics_category_fc3e5ef571.jpeg +0 -0
  147. package/dist/uploads/thumbnail_ergonomic_office_chair_1_c751cffb07.png +0 -0
  148. package/dist/uploads/thumbnail_home_garden_category_4f6eb3f8d6.jpeg +0 -0
  149. package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
  150. package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
  151. package/dist/uploads/thumbnail_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  152. package/dist/uploads/thumbnail_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  153. package/dist/uploads/thumbnail_predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
  154. package/dist/uploads/thumbnail_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  155. package/dist/uploads/thumbnail_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  156. package/dist/uploads/thumbnail_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  157. package/dist/uploads/thumbnail_wireless_headphones_1_fa75cd50c3.png +0 -0
  158. package/dist/uploads/thumbnail_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  159. package/dist/uploads/webby-commerce.png +0 -0
  160. package/dist/uploads/wireless_headphones_1_fa75cd50c3.png +0 -0
  161. package/dist/uploads/yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  162. /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
+