@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,3826 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { registerEcommerceActions, ensureEcommercePermission } = require('./utils/check-ecommerce-permission');
6
+ const { extendUserSchemaWithOtpFields } = require('./utils/extend-user-schema');
7
+
8
+ module.exports = async ({ strapi }) => {
9
+ try {
10
+ strapi.log.info('[webbycommerce] ========================================');
11
+ strapi.log.info('[webbycommerce] Bootstrapping plugin...');
12
+
13
+ // Extend user schema with OTP fields if they don't exist
14
+ try {
15
+ await extendUserSchemaWithOtpFields(strapi);
16
+ } catch (schemaError) {
17
+ strapi.log.warn('[webbycommerce] Could not automatically extend user schema:', schemaError.message);
18
+ strapi.log.warn(
19
+ '[webbycommerce] Please manually extend the user schema. See README for instructions.'
20
+ );
21
+ }
22
+
23
+ const disableSeeding =
24
+ process.env.STRAPI_PLUGIN_ADVANCED_ECOMMERCE_DISABLE_SEED_DEMO === 'true' ||
25
+ process.env.STRAPI_PLUGIN_ADVANCED_ECOMMERCE_DISABLE_SEED_DEMO === '1' ||
26
+ process.env.STRAPI_PLUGIN_ADVANCED_ECOMMERCE_DISABLE_SEED_DEMO === 'yes';
27
+
28
+ // Check for auto-seeding via environment variable or first run
29
+ // Only seed if explicitly requested and ensure the plugin is fully loaded
30
+ if (!disableSeeding && process.env.STRAPI_PLUGIN_ADVANCED_ECOMMERCE_SEED_DATA === 'true') {
31
+ try {
32
+ // Wait a bit to ensure all content types are registered
33
+ await new Promise(resolve => setTimeout(resolve, 1000));
34
+
35
+ strapi.log.info('[webbycommerce] Auto-seeding demo data as requested by environment variable...');
36
+
37
+ // Verify plugin is available before seeding
38
+ const pluginService = strapi.plugin('webbycommerce')?.service('service');
39
+ if (pluginService && typeof pluginService.seedDemoData === 'function') {
40
+ await pluginService.seedDemoData();
41
+ } else {
42
+ strapi.log.warn('[webbycommerce] Plugin service not available for seeding');
43
+ }
44
+ } catch (seedError) {
45
+ strapi.log.error('[webbycommerce] Auto-seeding failed:', seedError.message);
46
+ strapi.log.error('[webbycommerce] Stack:', seedError.stack);
47
+ }
48
+ } else if (disableSeeding && process.env.STRAPI_PLUGIN_ADVANCED_ECOMMERCE_SEED_DATA === 'true') {
49
+ strapi.log.info(
50
+ '[webbycommerce] Demo seeding is disabled by STRAPI_PLUGIN_ADVANCED_ECOMMERCE_DISABLE_SEED_DEMO; skipping auto-seed.'
51
+ );
52
+ }
53
+
54
+ // Ensure plugin content types are registered
55
+ const contentTypes = require('./content-types');
56
+ strapi.log.info('[webbycommerce] Content types loaded:', Object.keys(contentTypes));
57
+ if (contentTypes.components) {
58
+ strapi.log.info('[webbycommerce] Components loaded:', Object.keys(contentTypes.components));
59
+ }
60
+
61
+ // Verify routes are accessible
62
+ const routes = require('./routes');
63
+ strapi.log.info('[webbycommerce] Routes structure verified');
64
+ strapi.log.info('[webbycommerce] Full routes object:', JSON.stringify(routes, null, 2));
65
+ strapi.log.info('[webbycommerce] Content-API routes count: ' + (routes['content-api']?.routes?.length || 0));
66
+ strapi.log.info('[webbycommerce] Admin routes count: ' + (routes.admin?.routes?.length || 0));
67
+ strapi.log.info('[webbycommerce] Has content-api: ' + !!routes['content-api']);
68
+ strapi.log.info('[webbycommerce] Has admin: ' + !!routes.admin);
69
+
70
+ // Helper function to get route prefix from settings
71
+ const getRoutePrefix = async () => {
72
+ try {
73
+ const store = strapi.store({ type: 'plugin', name: 'webbycommerce' });
74
+ const value = (await store.get({ key: 'settings' })) || {};
75
+ return value.routePrefix || 'webbycommerce';
76
+ } catch (error) {
77
+ return 'webbycommerce';
78
+ }
79
+ };
80
+
81
+ // Helper function to check if a route is an admin route
82
+ const isAdminRoute = (path) => {
83
+ if (!path) return false;
84
+ // Admin routes typically start with /admin/ or are admin API routes
85
+ // Content-Type Builder, Upload, Users-Permissions admin routes, etc.
86
+ const adminRoutePatterns = [
87
+ '/admin/',
88
+ '/content-type-builder/',
89
+ '/upload/',
90
+ '/users-permissions/',
91
+ '/i18n/',
92
+ '/email/',
93
+ '/documentation/',
94
+ '/graphql',
95
+ ];
96
+ return adminRoutePatterns.some(pattern => path.startsWith(pattern));
97
+ };
98
+
99
+ // CRITICAL: Fix for content-type-builder path issue - MUST run FIRST before any other middleware
100
+ // This ensures the API directory structure exists before Strapi tries to write schema files
101
+ strapi.server.use(async (ctx, next) => {
102
+ // Only handle content-type-builder update-schema requests
103
+ if (ctx.path === '/content-type-builder/update-schema' && ctx.method === 'POST') {
104
+ try {
105
+ // Parse body manually if not already parsed
106
+ let body = ctx.request.body;
107
+ let bodyWasParsed = false;
108
+
109
+ if (!body || (typeof body === 'object' && Object.keys(body).length === 0)) {
110
+ try {
111
+ const contentType = ctx.request.header['content-type'] || '';
112
+ if (contentType.includes('application/json') && ctx.req && typeof ctx.req[Symbol.asyncIterator] === 'function') {
113
+ // Read the stream
114
+ const chunks = [];
115
+ for await (const chunk of ctx.req) {
116
+ chunks.push(chunk);
117
+ }
118
+ const rawBody = Buffer.concat(chunks).toString('utf8');
119
+
120
+ if (rawBody && rawBody.trim()) {
121
+ body = JSON.parse(rawBody);
122
+ ctx.request.body = body;
123
+ bodyWasParsed = true;
124
+ // Restore the stream for downstream middleware
125
+ const { Readable } = require('stream');
126
+ ctx.req = Readable.from([Buffer.from(rawBody)]);
127
+ strapi.log.info('[webbycommerce] EARLY: Manually parsed request body');
128
+ }
129
+ }
130
+ } catch (parseError) {
131
+ strapi.log.warn('[webbycommerce] EARLY: Could not parse body:', parseError.message);
132
+ }
133
+ }
134
+
135
+ body = body || {};
136
+
137
+ // Handle both nested (body.data) and flat (body) request structures
138
+ const data = body.data || body;
139
+ const contentTypes = data.contentTypes || [];
140
+ const components = data.components || [];
141
+
142
+ strapi.log.info('[webbycommerce] ===== EARLY: Processing content-type-builder update-schema request =====');
143
+ strapi.log.info('[webbycommerce] EARLY: Body type:', typeof body);
144
+ strapi.log.info('[webbycommerce] EARLY: Body keys:', Object.keys(body));
145
+ strapi.log.info('[webbycommerce] EARLY: Content types found:', contentTypes.length);
146
+ strapi.log.info('[webbycommerce] EARLY: Components found:', components.length);
147
+
148
+ // Get the Strapi app directory - try multiple possible locations
149
+ let appDir;
150
+ if (strapi.dirs && strapi.dirs.app && strapi.dirs.app.root) {
151
+ appDir = strapi.dirs.app.root;
152
+ } else if (strapi.dirs && strapi.dirs.root) {
153
+ appDir = strapi.dirs.root;
154
+ } else {
155
+ // Fallback: __dirname is server/src, so go up two levels to get project root
156
+ appDir = path.resolve(__dirname, '../..');
157
+ }
158
+
159
+ // Ensure strapi.dirs is set for Strapi's internal use
160
+ if (!strapi.dirs) {
161
+ strapi.dirs = {};
162
+ }
163
+ if (!strapi.dirs.app) {
164
+ strapi.dirs.app = {};
165
+ }
166
+ if (!strapi.dirs.app.root) {
167
+ strapi.dirs.app.root = appDir;
168
+ }
169
+
170
+ // Process each content type in the request
171
+ for (const contentType of contentTypes) {
172
+ if (contentType.uid && contentType.uid.startsWith('api::')) {
173
+ const uidParts = contentType.uid.split('::');
174
+ if (uidParts.length === 2) {
175
+ const apiAndType = uidParts[1].split('.');
176
+ if (apiAndType.length >= 2) {
177
+ const apiName = apiAndType[0];
178
+ const contentTypeName = apiAndType[1];
179
+
180
+ const apiDir = path.join(appDir, 'src', 'api', apiName);
181
+ const contentTypeDir = path.join(apiDir, 'content-types', contentTypeName);
182
+ const schemaPath = path.join(contentTypeDir, 'schema.json');
183
+
184
+ // Handle collection deletion
185
+ if (contentType.action === 'delete') {
186
+ strapi.log.info(`[webbycommerce] EARLY: Deleting collection: ${contentType.uid}`);
187
+
188
+ // Delete schema file
189
+ if (fs.existsSync(schemaPath)) {
190
+ fs.unlinkSync(schemaPath);
191
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Deleted schema file: ${schemaPath}`);
192
+ }
193
+
194
+ // Delete content type directory (optional - Strapi will handle cleanup)
195
+ if (fs.existsSync(contentTypeDir)) {
196
+ try {
197
+ fs.rmSync(contentTypeDir, { recursive: true, force: true });
198
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Deleted content type directory: ${contentTypeDir}`);
199
+ } catch (error) {
200
+ strapi.log.warn(`[webbycommerce] EARLY: Could not delete directory: ${error.message}`);
201
+ }
202
+ }
203
+
204
+ ctx.state.schemaFileCreated = true;
205
+ ctx.state.schemaDeleted = true;
206
+ continue; // Skip to next content type
207
+ }
208
+
209
+ // FORCE create directory structure
210
+ fs.mkdirSync(contentTypeDir, { recursive: true });
211
+
212
+ // Read existing schema to preserve any existing attributes
213
+ let existingSchema = {};
214
+ if (fs.existsSync(schemaPath)) {
215
+ try {
216
+ existingSchema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
217
+ } catch (e) {
218
+ existingSchema = {};
219
+ }
220
+ }
221
+
222
+ // Build complete schema from request data
223
+ // This ensures the schema file is complete and valid before Strapi processes it
224
+ // Start with existing attributes to preserve them
225
+ const attributes = { ...(existingSchema.attributes || {}) };
226
+
227
+ // Process all attributes from the request
228
+ if (contentType.attributes && Array.isArray(contentType.attributes)) {
229
+ for (const attr of contentType.attributes) {
230
+ const action = attr.action || 'update';
231
+
232
+ // Handle field deletion
233
+ if (action === 'delete' && attr.name) {
234
+ if (attributes[attr.name]) {
235
+ delete attributes[attr.name];
236
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Deleted attribute: ${attr.name}`);
237
+ } else {
238
+ strapi.log.warn(`[webbycommerce] EARLY: Attribute not found for deletion: ${attr.name}`);
239
+ }
240
+ continue; // Skip to next attribute
241
+ }
242
+
243
+ // Handle create/update
244
+ if (attr.name && attr.properties) {
245
+ // Build the attribute object from properties
246
+ const attributeDef = { ...attr.properties };
247
+
248
+ // Handle component types - ensure component references are correct
249
+ if (attributeDef.type === 'component') {
250
+ if (attributeDef.component) {
251
+ strapi.log.info(`[webbycommerce] EARLY: Processing component attribute: ${attr.name} -> ${attributeDef.component}`);
252
+ }
253
+ // Component attributes need specific structure
254
+ if (!attributeDef.repeatable) {
255
+ attributeDef.repeatable = false;
256
+ }
257
+ }
258
+
259
+ // Handle dynamiczone types
260
+ if (attributeDef.type === 'dynamiczone') {
261
+ if (Array.isArray(attributeDef.components)) {
262
+ strapi.log.info(`[webbycommerce] EARLY: Processing dynamiczone: ${attr.name} with ${attributeDef.components.length} components`);
263
+ }
264
+ }
265
+
266
+ // Handle relation types
267
+ if (attributeDef.type === 'relation') {
268
+ if (attributeDef.target) {
269
+ strapi.log.info(`[webbycommerce] EARLY: Processing relation: ${attr.name} -> ${attributeDef.target}`);
270
+ }
271
+ }
272
+
273
+ // Update/add the attribute
274
+ attributes[attr.name] = attributeDef;
275
+
276
+ strapi.log.info(`[webbycommerce] EARLY: ${action === 'create' ? 'Added' : 'Updated'} attribute: ${attr.name} (type: ${attributeDef.type || 'unknown'})`);
277
+ }
278
+ }
279
+ }
280
+
281
+ // Build the complete schema object matching Strapi's format
282
+ const schema = {
283
+ kind: contentType.kind || existingSchema.kind || 'collectionType',
284
+ collectionName: contentType.collectionName || existingSchema.collectionName || (contentType.kind === 'singleType' ? contentTypeName : `${contentTypeName}s`),
285
+ info: {
286
+ singularName: contentType.singularName || existingSchema.info?.singularName || contentTypeName,
287
+ pluralName: contentType.pluralName || existingSchema.info?.pluralName || (contentType.kind === 'singleType' ? contentTypeName : `${contentTypeName}s`),
288
+ displayName: contentType.displayName || contentType.modelName || existingSchema.info?.displayName || contentTypeName,
289
+ description: contentType.description || existingSchema.info?.description || '',
290
+ },
291
+ options: {
292
+ draftAndPublish: contentType.draftAndPublish !== undefined ? contentType.draftAndPublish : (existingSchema.options?.draftAndPublish !== undefined ? existingSchema.options.draftAndPublish : false),
293
+ },
294
+ pluginOptions: contentType.pluginOptions || existingSchema.pluginOptions || {
295
+ 'content-manager': {
296
+ visible: true
297
+ },
298
+ 'content-api': {
299
+ visible: true
300
+ }
301
+ },
302
+ attributes: attributes,
303
+ };
304
+
305
+ // Write the complete schema file
306
+ // This file will trigger Strapi's file watcher and cause auto-restart
307
+ // After restart, Strapi will read this file and register the collection with all fields/components
308
+ const schemaJson = JSON.stringify(schema, null, 2);
309
+ fs.writeFileSync(schemaPath, schemaJson, 'utf8');
310
+
311
+ // Verify the file was written correctly and is valid JSON
312
+ if (fs.existsSync(schemaPath)) {
313
+ try {
314
+ // Verify it's valid JSON and can be read back
315
+ const verifySchema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
316
+ const fileStats = fs.statSync(schemaPath);
317
+
318
+ strapi.log.info(`[webbycommerce] ========================================`);
319
+ strapi.log.info(`[webbycommerce] ✓ COLLECTION SCHEMA CREATED/UPDATED`);
320
+ strapi.log.info(`[webbycommerce] ========================================`);
321
+ strapi.log.info(`[webbycommerce] ✓ File: ${schemaPath}`);
322
+ strapi.log.info(`[webbycommerce] ✓ File size: ${fileStats.size} bytes`);
323
+ strapi.log.info(`[webbycommerce] ✓ Schema is valid JSON`);
324
+ strapi.log.info(`[webbycommerce] ✓ Schema kind: ${verifySchema.kind}`);
325
+ strapi.log.info(`[webbycommerce] ✓ Collection name: ${verifySchema.collectionName}`);
326
+ strapi.log.info(`[webbycommerce] ✓ Display name: ${verifySchema.info?.displayName || 'N/A'}`);
327
+ strapi.log.info(`[webbycommerce] ✓ Total attributes: ${Object.keys(verifySchema.attributes || {}).length}`);
328
+
329
+ // List all attributes with their types
330
+ const attrNames = Object.keys(verifySchema.attributes || {});
331
+ if (attrNames.length > 0) {
332
+ strapi.log.info(`[webbycommerce] ✓ Attributes list:`);
333
+ attrNames.forEach(attrName => {
334
+ const attr = verifySchema.attributes[attrName];
335
+ const attrType = attr.type || 'unknown';
336
+ const attrInfo = attrType === 'component' ? `component: ${attr.component}` :
337
+ attrType === 'dynamiczone' ? `dynamiczone: ${(attr.components || []).join(', ')}` :
338
+ attrType === 'relation' ? `relation: ${attr.target}` :
339
+ attrType;
340
+ strapi.log.info(`[webbycommerce] - ${attrName}: ${attrInfo}`);
341
+ });
342
+ } else {
343
+ strapi.log.warn(`[webbycommerce] ⚠ No attributes found - this is a new empty collection`);
344
+ }
345
+
346
+ strapi.log.info(`[webbycommerce] ✓ File will trigger auto-restart`);
347
+ strapi.log.info(`[webbycommerce] ✓ After restart, collection will be registered with all fields/components`);
348
+ strapi.log.info(`[webbycommerce] ========================================`);
349
+
350
+ // Ensure file permissions are correct
351
+ fs.chmodSync(schemaPath, 0o644);
352
+
353
+ // Touch the file to ensure file watcher detects the change
354
+ const now = new Date();
355
+ fs.utimesSync(schemaPath, now, now);
356
+
357
+ // Mark that we've successfully created the schema file
358
+ ctx.state.schemaFileCreated = true;
359
+ ctx.state.schemaPath = schemaPath;
360
+ ctx.state.contentTypeUid = contentType.uid;
361
+
362
+ } catch (verifyError) {
363
+ strapi.log.error(`[webbycommerce] ✗ Schema file verification failed: ${verifyError.message}`);
364
+ strapi.log.error(`[webbycommerce] ✗ Stack: ${verifyError.stack}`);
365
+ }
366
+ } else {
367
+ strapi.log.error(`[webbycommerce] ✗ Schema file was not created: ${schemaPath}`);
368
+ }
369
+
370
+ // Also ensure controllers, services, and routes directories exist
371
+ const controllersDir = path.join(apiDir, 'controllers', contentTypeName);
372
+ const servicesDir = path.join(apiDir, 'services', contentTypeName);
373
+ const routesDir = path.join(apiDir, 'routes', contentTypeName);
374
+
375
+ [controllersDir, servicesDir, routesDir].forEach(dir => {
376
+ if (!fs.existsSync(dir)) {
377
+ fs.mkdirSync(dir, { recursive: true });
378
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Created directory: ${dir}`);
379
+ }
380
+ });
381
+ }
382
+ }
383
+ }
384
+ }
385
+
386
+ // If we successfully created/updated/deleted schema files (content types or components), return success early
387
+ // The file watcher will trigger auto-restart, and after restart Strapi will read the schema files
388
+ const hasContentTypes = (ctx.state.schemaFileCreated || ctx.state.schemaDeleted) && contentTypes.length > 0;
389
+ const hasComponents = ctx.state.componentsCreated === true || ctx.state.componentsDeleted === true;
390
+
391
+ strapi.log.info(`[webbycommerce] EARLY: Checking early return conditions...`);
392
+ strapi.log.info(`[webbycommerce] EARLY: hasContentTypes=${hasContentTypes}, hasComponents=${hasComponents}`);
393
+ strapi.log.info(`[webbycommerce] EARLY: ctx.state.schemaFileCreated=${ctx.state.schemaFileCreated}, ctx.state.componentsCreated=${ctx.state.componentsCreated}`);
394
+ strapi.log.info(`[webbycommerce] EARLY: contentTypes.length=${contentTypes.length}, components.length=${components.length}`);
395
+
396
+ if (hasContentTypes || hasComponents) {
397
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Schema file(s) created successfully`);
398
+ if (hasContentTypes) {
399
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Created ${contentTypes.length} content type(s)`);
400
+ }
401
+ if (hasComponents) {
402
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Created ${components.length} component(s)`);
403
+ }
404
+ strapi.log.info(`[webbycommerce] EARLY: ✓ File watcher will detect change and trigger auto-restart`);
405
+ strapi.log.info(`[webbycommerce] EARLY: ✓ After restart, collections and components will be automatically registered with all fields`);
406
+
407
+ // Return success response immediately
408
+ // The schema files are already written, so we don't need Strapi to process them again
409
+ // This prevents the path undefined error
410
+ ctx.status = 200;
411
+ // Set headers to ensure Strapi's admin panel detects the change and triggers auto-reload
412
+ ctx.set('Content-Type', 'application/json');
413
+ ctx.body = {
414
+ data: {
415
+ contentTypes: contentTypes.map(ct => {
416
+ const uidParts = ct.uid.split('::');
417
+ const apiAndType = uidParts.length === 2 ? uidParts[1].split('.') : [];
418
+ return {
419
+ uid: ct.uid,
420
+ apiID: ct.uid,
421
+ schema: {
422
+ kind: ct.kind || 'collectionType',
423
+ collectionName: ct.collectionName || (ct.kind === 'singleType' ? apiAndType[1] : `${apiAndType[1]}s`),
424
+ info: {
425
+ singularName: ct.singularName || apiAndType[1],
426
+ pluralName: ct.pluralName || (ct.kind === 'singleType' ? apiAndType[1] : `${apiAndType[1]}s`),
427
+ displayName: ct.displayName || ct.modelName || apiAndType[1],
428
+ description: ct.description || '',
429
+ },
430
+ options: {
431
+ draftAndPublish: ct.draftAndPublish !== undefined ? ct.draftAndPublish : false,
432
+ },
433
+ }
434
+ };
435
+ }),
436
+ components: (components || []).map(comp => {
437
+ const uidParts = comp.uid ? comp.uid.split('.') : [];
438
+ return {
439
+ uid: comp.uid,
440
+ category: uidParts[0] || '',
441
+ apiID: comp.uid,
442
+ schema: {
443
+ collectionName: comp.collectionName || ('components_' + comp.uid.replace(/\./g, '_')),
444
+ info: {
445
+ displayName: comp.displayName || comp.modelName || uidParts[1] || 'New Component',
446
+ description: comp.description || '',
447
+ },
448
+ }
449
+ };
450
+ })
451
+ }
452
+ };
453
+
454
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Success response sent - request handled`);
455
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Returning early to prevent Strapi from processing request again`);
456
+ return; // Don't call next() - we've handled the request successfully
457
+ } else {
458
+ strapi.log.warn(`[webbycommerce] EARLY: ⚠ Not returning early - conditions not met`);
459
+ strapi.log.warn(`[webbycommerce] EARLY: ⚠ schemaFileCreated=${ctx.state.schemaFileCreated}, componentsCreated=${ctx.state.componentsCreated}`);
460
+ strapi.log.warn(`[webbycommerce] EARLY: ⚠ contentTypes.length=${contentTypes.length}, components.length=${components.length}`);
461
+ }
462
+ } catch (error) {
463
+ strapi.log.error('[webbycommerce] EARLY: Error in content-type-builder fix:', error.message);
464
+ strapi.log.error('[webbycommerce] EARLY: Stack:', error.stack);
465
+ }
466
+ }
467
+
468
+ return next();
469
+ });
470
+
471
+ // Fix for JSON field validation - sanitize empty strings to null for JSON fields
472
+ // This prevents "invalid input syntax for type json" errors in PostgreSQL
473
+ strapi.server.use(async (ctx, next) => {
474
+ // Handle content-manager requests (create, update, publish, bulk operations)
475
+ if (ctx.path.includes('/content-manager/collection-types/') &&
476
+ (ctx.method === 'POST' || ctx.method === 'PUT') &&
477
+ ctx.request.body) {
478
+ try {
479
+ // Extract content type UID from path (handles both regular and actions paths)
480
+ const match = ctx.path.match(/collection-types\/([^\/\?]+)/);
481
+ const contentTypeUid = match?.[1];
482
+
483
+ if (contentTypeUid && contentTypeUid.startsWith('api::')) {
484
+ // Get the content type schema to check field types
485
+ const contentType = strapi.contentTypes[contentTypeUid];
486
+ if (contentType && contentType.attributes) {
487
+ const body = ctx.request.body;
488
+ let modified = false;
489
+
490
+ // Helper function to sanitize a value for JSON field
491
+ const sanitizeJsonValue = (value, fieldName) => {
492
+ if (value === '' || value === '""') {
493
+ return null;
494
+ }
495
+ if (typeof value === 'string' && value.trim() === '') {
496
+ return null;
497
+ }
498
+ if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {
499
+ try {
500
+ return JSON.parse(value);
501
+ } catch (e) {
502
+ strapi.log.warn(`[webbycommerce] Failed to parse JSON string for field "${fieldName}", using null`);
503
+ return null;
504
+ }
505
+ }
506
+ return value;
507
+ };
508
+
509
+ // Sanitize JSON fields - convert empty strings to null
510
+ for (const [fieldName, fieldValue] of Object.entries(body)) {
511
+ // Skip metadata fields
512
+ if (fieldName === 'id' || fieldName === 'documentId' || fieldName.startsWith('_') ||
513
+ fieldName === 'createdAt' || fieldName === 'updatedAt' ||
514
+ fieldName === 'publishedAt' || fieldName === 'createdBy' || fieldName === 'updatedBy') {
515
+ continue;
516
+ }
517
+
518
+ const attribute = contentType.attributes[fieldName];
519
+ if (attribute && attribute.type === 'json') {
520
+ const sanitizedValue = sanitizeJsonValue(fieldValue, fieldName);
521
+ if (sanitizedValue !== fieldValue) {
522
+ body[fieldName] = sanitizedValue;
523
+ modified = true;
524
+ strapi.log.info(`[webbycommerce] Sanitized JSON field "${fieldName}": "${fieldValue}" -> ${sanitizedValue === null ? 'null' : 'parsed JSON'}`);
525
+ }
526
+ }
527
+ }
528
+
529
+ if (modified) {
530
+ strapi.log.info(`[webbycommerce] ✓ Sanitized JSON fields in content-manager request for ${contentTypeUid}`);
531
+ }
532
+ } else {
533
+ strapi.log.debug(`[webbycommerce] Content type ${contentTypeUid} not found or has no attributes`);
534
+ }
535
+ }
536
+ } catch (error) {
537
+ strapi.log.warn(`[webbycommerce] Error sanitizing JSON fields:`, error.message);
538
+ // Don't block the request - let Strapi handle it
539
+ }
540
+ }
541
+
542
+ return next();
543
+ });
544
+
545
+ // Handle custom route prefix for all content-api endpoints
546
+ // This middleware rewrites custom prefix paths to default prefix so Strapi's routing works
547
+ // Also supports /api/route pattern (without prefix) for convenience
548
+ strapi.server.use(async (ctx, next) => {
549
+ // Skip admin routes - let Strapi handle them
550
+ if (isAdminRoute(ctx.path)) {
551
+ return next();
552
+ }
553
+ const routePrefix = await getRoutePrefix();
554
+ const defaultBasePath = `/api/webbycommerce`;
555
+
556
+ // Skip Strapi core auth routes - these should not be rewritten
557
+ if (ctx.path.startsWith('/api/auth/local') || ctx.path.startsWith('/api/auth/')) {
558
+ return next();
559
+ }
560
+
561
+ // List of known plugin route segments (without leading slash)
562
+ const pluginRoutes = new Set([
563
+ 'products', 'product-variants', 'product-attributes', 'product-attribute-values',
564
+ 'product-categories', 'tags', 'cart', 'addresses', 'wishlist', 'compare',
565
+ 'orders', 'checkout', 'payments', 'shipping', 'auth', 'health'
566
+ ]);
567
+
568
+ // Check if path starts with /api/ but not /api/webbycommerce/
569
+ if (ctx.path.startsWith('/api/') && !ctx.path.startsWith(defaultBasePath + '/') && !ctx.path.startsWith('/api/webbycommerce/')) {
570
+ const pathAfterApi = ctx.path.substring(5); // Remove '/api/'
571
+ const firstSegment = pathAfterApi.split('/')[0];
572
+
573
+ // Check if first segment is a known plugin route
574
+ if (pluginRoutes.has(firstSegment)) {
575
+ // Store original path before rewriting
576
+ ctx.state.originalPath = ctx.path;
577
+ // Rewrite /api/route to /api/webbycommerce/route
578
+ ctx.path = `${defaultBasePath}/${pathAfterApi}`;
579
+ return next();
580
+ }
581
+ }
582
+
583
+ // Handle custom prefix paths (if different from default)
584
+ if (routePrefix !== 'webbycommerce') {
585
+ const customBasePath = `/api/${routePrefix}`;
586
+
587
+ // Rewrite custom prefix paths to default paths for route matching
588
+ if (ctx.path.startsWith(customBasePath)) {
589
+ // Store original path before rewriting
590
+ ctx.state.originalPath = ctx.path;
591
+ const remainingPath = ctx.path.replace(customBasePath, '');
592
+ ctx.path = `${defaultBasePath}${remainingPath}`;
593
+ }
594
+ }
595
+
596
+ return next();
597
+ });
598
+
599
+
600
+
601
+ // Lightweight health endpoint mounted via Koa middleware.
602
+ // This bypasses routing quirks and provides public access for health checks.
603
+ // Supports both default and custom route prefixes.
604
+ strapi.server.use(async (ctx, next) => {
605
+ // Skip admin routes - let Strapi handle them
606
+ if (isAdminRoute(ctx.path)) {
607
+ return next();
608
+ }
609
+
610
+ const routePrefix = await getRoutePrefix();
611
+ const defaultPath = `/api/webbycommerce/health`;
612
+ const customPath = `/api/${routePrefix}/health`;
613
+ const legacyPath = `/${routePrefix}/health`;
614
+
615
+ if (
616
+ ctx.method === 'GET' &&
617
+ (ctx.path === defaultPath ||
618
+ ctx.path === customPath ||
619
+ ctx.path === legacyPath ||
620
+ ctx.path === '/webbycommerce/health' ||
621
+ ctx.path === '/api/webbycommerce/health')
622
+ ) {
623
+ // Health check is public - no permission required
624
+ ctx.set('Content-Type', 'application/json; charset=utf-8');
625
+ ctx.body = {
626
+ status: 'ok',
627
+ plugin: 'webbycommerce',
628
+ message: 'Ecommerce plugin is running',
629
+ };
630
+ return;
631
+ }
632
+
633
+ return next();
634
+ });
635
+
636
+ // Enforce login/register method and ecommerce permission for core Strapi auth endpoints.
637
+ // - When method is "otp": block /api/auth/local and /api/auth/local/register
638
+ // - When method is "default": allow them but check ecommerce permission
639
+ strapi.server.use(async (ctx, next) => {
640
+ // Skip admin routes - let Strapi handle them
641
+ if (isAdminRoute(ctx.path)) {
642
+ return next();
643
+ }
644
+
645
+ if (
646
+ ctx.method === 'POST' &&
647
+ (ctx.path === '/api/auth/local' || ctx.path === '/api/auth/local/register')
648
+ ) {
649
+ try {
650
+ const store = strapi.store({ type: 'plugin', name: 'webbycommerce' });
651
+ const value = (await store.get({ key: 'settings' })) || {};
652
+ const method = value.loginRegisterMethod || 'default';
653
+
654
+ if (method === 'otp') {
655
+ // When OTP mode is enabled, core email/password endpoints should not be used
656
+ ctx.badRequest(
657
+ 'Authentication method is set to OTP. Please use the OTP login/register endpoints or the unified /auth/unified endpoint.'
658
+ );
659
+ return;
660
+ }
661
+
662
+ // If method is 'both', allow both OTP and default methods via unified endpoint
663
+ // But still allow default login/register endpoints
664
+ if (method === 'both') {
665
+ // Allow default endpoints to work when 'both' is selected
666
+ // The unified endpoint will handle both methods
667
+ }
668
+
669
+ // For registration, allow it to proceed (it's a public endpoint)
670
+ // For login, check ecommerce permission
671
+ if (ctx.path === '/api/auth/local/register') {
672
+ // Registration is public - allow it to proceed to Strapi's core handler
673
+ return next();
674
+ }
675
+
676
+ // For login, check ecommerce permission
677
+ if (ctx.path === '/api/auth/local') {
678
+ const hasPermission = await ensureEcommercePermission(ctx);
679
+ if (!hasPermission) {
680
+ return; // ensureEcommercePermission already sent the response
681
+ }
682
+ }
683
+ } catch (error) {
684
+ // If settings cannot be read, fall back to Strapi default behavior
685
+ strapi.log.error(
686
+ '[webbycommerce] Failed to read loginRegisterMethod for auth guard:',
687
+ error
688
+ );
689
+ }
690
+ }
691
+
692
+ return next();
693
+ });
694
+
695
+ // OTP auth routes (login/register and verify-otp) mounted via Koa middleware.
696
+ // This ensures they work reliably with both default and custom route prefixes.
697
+ strapi.server.use(async (ctx, next) => {
698
+ // Skip admin routes - let Strapi handle them
699
+ if (isAdminRoute(ctx.path)) {
700
+ return next();
701
+ }
702
+
703
+ const routePrefix = await getRoutePrefix();
704
+ const originalPath = ctx.state.originalPath || ctx.path;
705
+
706
+ const loginPaths = new Set([
707
+ '/api/webbycommerce/auth/login-register',
708
+ `/api/${routePrefix}/auth/login-register`,
709
+ '/webbycommerce/auth/login-register',
710
+ `/${routePrefix}/auth/login-register`,
711
+ ]);
712
+
713
+ const verifyPaths = new Set([
714
+ '/api/webbycommerce/auth/verify-otp',
715
+ `/api/${routePrefix}/auth/verify-otp`,
716
+ '/webbycommerce/auth/verify-otp',
717
+ `/${routePrefix}/auth/verify-otp`,
718
+ ]);
719
+
720
+ const methodPaths = new Set([
721
+ '/api/webbycommerce/auth/method',
722
+ `/api/${routePrefix}/auth/method`,
723
+ '/webbycommerce/auth/method',
724
+ `/${routePrefix}/auth/method`,
725
+ ]);
726
+
727
+ const unifiedAuthPaths = new Set([
728
+ '/api/webbycommerce/auth/unified',
729
+ `/api/${routePrefix}/auth/unified`,
730
+ '/webbycommerce/auth/unified',
731
+ `/${routePrefix}/auth/unified`,
732
+ ]);
733
+
734
+ const profilePaths = new Set([
735
+ '/api/webbycommerce/auth/profile',
736
+ `/api/${routePrefix}/auth/profile`,
737
+ '/webbycommerce/auth/profile',
738
+ `/${routePrefix}/auth/profile`,
739
+ ]);
740
+
741
+ // POST /auth/login-register
742
+ if (ctx.method === 'POST' && loginPaths.has(ctx.path)) {
743
+ // Mark this as a content-api request for the plugin
744
+ ctx.state.route = {
745
+ info: {
746
+ type: 'content-api',
747
+ pluginName: 'webbycommerce',
748
+ },
749
+ };
750
+
751
+ const authController = strapi
752
+ .plugin('webbycommerce')
753
+ .controller('auth');
754
+
755
+ if (authController && typeof authController.loginOrRegister === 'function') {
756
+ await authController.loginOrRegister(ctx);
757
+ return;
758
+ }
759
+ }
760
+
761
+ // POST /auth/verify-otp
762
+ if (ctx.method === 'POST' && verifyPaths.has(ctx.path)) {
763
+ ctx.state.route = {
764
+ info: {
765
+ type: 'content-api',
766
+ pluginName: 'webbycommerce',
767
+ },
768
+ };
769
+
770
+ const authController = strapi
771
+ .plugin('webbycommerce')
772
+ .controller('auth');
773
+
774
+ if (authController && typeof authController.verifyOtp === 'function') {
775
+ await authController.verifyOtp(ctx);
776
+ return;
777
+ }
778
+ }
779
+
780
+ // GET /auth/method
781
+ if (ctx.method === 'GET' && methodPaths.has(ctx.path)) {
782
+ ctx.state.route = {
783
+ info: {
784
+ type: 'content-api',
785
+ pluginName: 'webbycommerce',
786
+ },
787
+ };
788
+
789
+ const authController = strapi
790
+ .plugin('webbycommerce')
791
+ .controller('auth');
792
+
793
+ if (authController && typeof authController.getAuthMethod === 'function') {
794
+ await authController.getAuthMethod(ctx);
795
+ return;
796
+ }
797
+ }
798
+
799
+ // POST /auth/unified
800
+ if (ctx.method === 'POST' && unifiedAuthPaths.has(ctx.path)) {
801
+ ctx.state.route = {
802
+ info: {
803
+ type: 'content-api',
804
+ pluginName: 'webbycommerce',
805
+ },
806
+ };
807
+
808
+ // Parse request body if not already parsed
809
+ if (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0)) {
810
+ try {
811
+ const contentType = ctx.request.header['content-type'] || '';
812
+
813
+ if (contentType.includes('application/json')) {
814
+ // Read raw body from request stream
815
+ const chunks = [];
816
+ for await (const chunk of ctx.req) {
817
+ chunks.push(chunk);
818
+ }
819
+ const rawBody = Buffer.concat(chunks).toString('utf8');
820
+
821
+ if (rawBody && rawBody.trim()) {
822
+ ctx.request.body = JSON.parse(rawBody);
823
+ strapi.log.debug(`[webbycommerce] Parsed request body for unified auth:`, ctx.request.body);
824
+ }
825
+ }
826
+ } catch (error) {
827
+ strapi.log.error(`[webbycommerce] Failed to parse request body for unified auth:`, error.message);
828
+ // Continue - controller will handle validation errors
829
+ }
830
+ }
831
+
832
+ const authController = strapi
833
+ .plugin('webbycommerce')
834
+ .controller('auth');
835
+
836
+ if (authController && typeof authController.unifiedAuth === 'function') {
837
+ await authController.unifiedAuth(ctx);
838
+ return;
839
+ }
840
+ }
841
+
842
+ // GET /auth/profile and PUT /auth/profile
843
+ if ((ctx.method === 'GET' || ctx.method === 'PUT') && profilePaths.has(ctx.path)) {
844
+ ctx.state.route = {
845
+ info: {
846
+ type: 'content-api',
847
+ pluginName: 'webbycommerce',
848
+ },
849
+ };
850
+
851
+ // Authenticate user via JWT token before calling controller
852
+ const authHeader = ctx.request.header.authorization;
853
+ if (authHeader && authHeader.startsWith('Bearer ')) {
854
+ const token = authHeader.replace('Bearer ', '').trim();
855
+ if (token) {
856
+ try {
857
+ // Verify JWT token
858
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
859
+ if (jwtService && typeof jwtService.verify === 'function') {
860
+ const decoded = await jwtService.verify(token);
861
+
862
+ if (decoded && decoded.id) {
863
+ // Fetch user from database
864
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
865
+ where: { id: decoded.id },
866
+ populate: ['role'],
867
+ });
868
+
869
+ if (user) {
870
+ ctx.state.user = user;
871
+ strapi.log.debug(`[webbycommerce] User authenticated: ${user.id}`);
872
+ } else {
873
+ strapi.log.warn(`[webbycommerce] User not found for ID: ${decoded.id}`);
874
+ }
875
+ }
876
+ }
877
+ } catch (error) {
878
+ // JWT verification failure is expected for public endpoints - log at debug level
879
+ strapi.log.debug(`[webbycommerce] JWT verification failed:`, error.message);
880
+ // Continue to controller - it will handle the unauthorized response
881
+ }
882
+ }
883
+ }
884
+
885
+ const authController = strapi
886
+ .plugin('webbycommerce')
887
+ .controller('auth');
888
+
889
+ if (ctx.method === 'GET' && authController && typeof authController.getProfile === 'function') {
890
+ await authController.getProfile(ctx);
891
+ return;
892
+ }
893
+
894
+ if (ctx.method === 'PUT' && authController && typeof authController.updateProfile === 'function') {
895
+ await authController.updateProfile(ctx);
896
+ return;
897
+ }
898
+ }
899
+
900
+ // Handle address routes
901
+ // Check both custom prefix and default prefix (after rewrite)
902
+ const customAddressPath = `/api/${routePrefix}/addresses`;
903
+ const defaultAddressPath = `/api/webbycommerce/addresses`;
904
+ const isAddressRoute =
905
+ ctx.path === customAddressPath ||
906
+ ctx.path.startsWith(`${customAddressPath}/`) ||
907
+ ctx.path === defaultAddressPath ||
908
+ ctx.path.startsWith(`${defaultAddressPath}/`) ||
909
+ originalPath === customAddressPath ||
910
+ originalPath.startsWith(`${customAddressPath}/`);
911
+
912
+ if (isAddressRoute) {
913
+ // Extract ID from path if present
914
+ let addressId = null;
915
+ const pathMatch = ctx.path.match(/\/addresses\/([^\/]+)/);
916
+ if (pathMatch) {
917
+ addressId = pathMatch[1];
918
+ // Set ctx.params.id for controller access
919
+ if (!ctx.params) {
920
+ ctx.params = {};
921
+ }
922
+ ctx.params.id = addressId;
923
+ }
924
+
925
+ // Authenticate user for address routes
926
+ const authHeader = ctx.request.header.authorization;
927
+ if (authHeader && authHeader.startsWith('Bearer ')) {
928
+ const token = authHeader.replace('Bearer ', '').trim();
929
+ if (token) {
930
+ try {
931
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
932
+ if (jwtService && typeof jwtService.verify === 'function') {
933
+ const decoded = await jwtService.verify(token);
934
+
935
+ if (decoded && decoded.id) {
936
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
937
+ where: { id: decoded.id },
938
+ populate: ['role'],
939
+ });
940
+
941
+ if (user) {
942
+ ctx.state.user = user;
943
+ }
944
+ }
945
+ }
946
+ } catch (error) {
947
+ // JWT verification failure is expected for public endpoints - log at debug level
948
+ strapi.log.debug(`[webbycommerce] JWT verification failed for address route:`, error.message);
949
+ }
950
+ }
951
+ }
952
+
953
+ // Check ecommerce permission
954
+ const hasPermission = await ensureEcommercePermission(ctx);
955
+ if (!hasPermission) {
956
+ return;
957
+ }
958
+
959
+ // Parse request body for POST/PUT requests if not already parsed
960
+ const method = ctx.method.toLowerCase();
961
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
962
+ try {
963
+ // Read and parse JSON body manually since we're intercepting before body parser
964
+ const contentType = ctx.request.header['content-type'] || '';
965
+
966
+ if (contentType.includes('application/json')) {
967
+ // Read raw body from request stream
968
+ const chunks = [];
969
+ for await (const chunk of ctx.req) {
970
+ chunks.push(chunk);
971
+ }
972
+ const rawBody = Buffer.concat(chunks).toString('utf8');
973
+
974
+ if (rawBody && rawBody.trim()) {
975
+ ctx.request.body = JSON.parse(rawBody);
976
+ strapi.log.debug(`[webbycommerce] Parsed request body:`, ctx.request.body);
977
+ }
978
+ }
979
+ } catch (error) {
980
+ strapi.log.error(`[webbycommerce] Failed to parse request body:`, error.message);
981
+ // Continue - controller will handle validation errors
982
+ }
983
+ }
984
+
985
+ const addressController = strapi
986
+ .plugin('webbycommerce')
987
+ .controller('address');
988
+
989
+ if (addressController) {
990
+ if (method === 'get' && !addressId && typeof addressController.getAddresses === 'function') {
991
+ await addressController.getAddresses(ctx);
992
+ return;
993
+ }
994
+
995
+ if (method === 'get' && addressId && typeof addressController.getAddress === 'function') {
996
+ await addressController.getAddress(ctx);
997
+ return;
998
+ }
999
+
1000
+ if (method === 'post' && typeof addressController.createAddress === 'function') {
1001
+ await addressController.createAddress(ctx);
1002
+ return;
1003
+ }
1004
+
1005
+ if (method === 'put' && addressId && typeof addressController.updateAddress === 'function') {
1006
+ await addressController.updateAddress(ctx);
1007
+ return;
1008
+ }
1009
+
1010
+ if (method === 'delete' && addressId && typeof addressController.deleteAddress === 'function') {
1011
+ await addressController.deleteAddress(ctx);
1012
+ return;
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ // Handle product routes
1018
+ // Check both custom prefix and default prefix (after rewrite)
1019
+ const customProductPath = `/api/${routePrefix}/products`;
1020
+ const defaultProductPath = `/api/webbycommerce/products`;
1021
+ const isProductRoute =
1022
+ ctx.path === customProductPath ||
1023
+ ctx.path.startsWith(`${customProductPath}/`) ||
1024
+ ctx.path === defaultProductPath ||
1025
+ ctx.path.startsWith(`${defaultProductPath}/`) ||
1026
+ originalPath === customProductPath ||
1027
+ originalPath.startsWith(`${customProductPath}/`);
1028
+
1029
+ if (isProductRoute) {
1030
+ // Parse product route segments safely so we can support:
1031
+ // - GET /products
1032
+ // - GET /products/:id
1033
+ // - GET /products/:id/related
1034
+ // - GET /products/slug/:slug
1035
+ // - GET /products/categories
1036
+ // - GET /products/tags
1037
+ // - GET /products/attributes
1038
+ // - POST /products
1039
+ // - PUT /products/:id
1040
+ // - DELETE /products/:id
1041
+ const pathParts = (ctx.path || '').split('/').filter(Boolean);
1042
+ const productsIndex = pathParts.lastIndexOf('products');
1043
+ const next1 = productsIndex >= 0 ? pathParts[productsIndex + 1] : null;
1044
+ const next2 = productsIndex >= 0 ? pathParts[productsIndex + 2] : null;
1045
+
1046
+ const reserved = new Set(['attributes', 'categories', 'tags', 'slug', 'bulk']);
1047
+ const productAction = next1 && reserved.has(next1) ? next1 : null;
1048
+ const productId = next1 && !productAction ? next1 : null;
1049
+ const isRelated = Boolean(productId && next2 === 'related');
1050
+ const slugValue = productAction === 'slug' ? next2 : null;
1051
+ const isNumericId = (value) => typeof value === 'string' && /^[0-9]+$/.test(value);
1052
+
1053
+ // Set ctx.params for controller access
1054
+ if (!ctx.params) {
1055
+ ctx.params = {};
1056
+ }
1057
+ if (productId) {
1058
+ ctx.params.id = productId;
1059
+ }
1060
+ if (slugValue) {
1061
+ ctx.params.slug = slugValue;
1062
+ }
1063
+
1064
+ // Authenticate user for product routes
1065
+ const authHeader = ctx.request.header.authorization;
1066
+ if (authHeader && authHeader.startsWith('Bearer ')) {
1067
+ const token = authHeader.replace('Bearer ', '').trim();
1068
+ if (token) {
1069
+ try {
1070
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
1071
+ if (jwtService && typeof jwtService.verify === 'function') {
1072
+ const decoded = await jwtService.verify(token);
1073
+
1074
+ if (decoded && decoded.id) {
1075
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
1076
+ where: { id: decoded.id },
1077
+ populate: ['role'],
1078
+ });
1079
+
1080
+ if (user) {
1081
+ ctx.state.user = user;
1082
+ }
1083
+ }
1084
+ }
1085
+ } catch (error) {
1086
+ // JWT verification failure is expected for public endpoints - log at debug level
1087
+ strapi.log.debug(`[webbycommerce] JWT verification failed for product route:`, error.message);
1088
+ }
1089
+ }
1090
+ }
1091
+
1092
+ // Check ecommerce permission
1093
+ const hasPermission = await ensureEcommercePermission(ctx);
1094
+ if (!hasPermission) {
1095
+ return;
1096
+ }
1097
+
1098
+ // Parse request body for POST/PUT requests if not already parsed
1099
+ const method = ctx.method.toLowerCase();
1100
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
1101
+ try {
1102
+ // Read and parse JSON body manually since we're intercepting before body parser
1103
+ const contentType = ctx.request.header['content-type'] || '';
1104
+
1105
+ if (contentType.includes('application/json')) {
1106
+ // Read raw body from request stream
1107
+ const chunks = [];
1108
+ for await (const chunk of ctx.req) {
1109
+ chunks.push(chunk);
1110
+ }
1111
+ const rawBody = Buffer.concat(chunks).toString('utf8');
1112
+
1113
+ if (rawBody && rawBody.trim()) {
1114
+ ctx.request.body = JSON.parse(rawBody);
1115
+ strapi.log.debug(`[webbycommerce] Parsed request body:`, ctx.request.body);
1116
+ }
1117
+ }
1118
+ } catch (error) {
1119
+ strapi.log.error(`[webbycommerce] Failed to parse request body:`, error.message);
1120
+ // Continue - controller will handle validation errors
1121
+ }
1122
+ }
1123
+
1124
+ const productController = strapi
1125
+ .plugin('webbycommerce')
1126
+ .controller('product');
1127
+
1128
+ if (productController) {
1129
+ // Handle special product routes first (before generic :id routes)
1130
+ if (method === 'get' && productAction === 'attributes' && typeof productController.getAttributes === 'function') {
1131
+ await productController.getAttributes(ctx);
1132
+ return;
1133
+ }
1134
+
1135
+ if (method === 'get' && productAction === 'categories' && typeof productController.getCategories === 'function') {
1136
+ await productController.getCategories(ctx);
1137
+ return;
1138
+ }
1139
+
1140
+ if (method === 'get' && productAction === 'tags' && typeof productController.getTags === 'function') {
1141
+ await productController.getTags(ctx);
1142
+ return;
1143
+ }
1144
+
1145
+ if (method === 'get' && productAction === 'slug' && slugValue && typeof productController.getProductBySlug === 'function') {
1146
+ await productController.getProductBySlug(ctx);
1147
+ return;
1148
+ }
1149
+
1150
+ if (method === 'get' && isRelated && typeof productController.getRelatedProducts === 'function') {
1151
+ await productController.getRelatedProducts(ctx);
1152
+ return;
1153
+ }
1154
+
1155
+ if (method === 'get' && !productId && !productAction && typeof productController.getProducts === 'function') {
1156
+ await productController.getProducts(ctx);
1157
+ return;
1158
+ }
1159
+
1160
+ // GET /products/:slug (slug OR numeric id)
1161
+ if (method === 'get' && productId && !isRelated) {
1162
+ if (isNumericId(productId) && typeof productController.getProduct === 'function') {
1163
+ await productController.getProduct(ctx);
1164
+ return;
1165
+ }
1166
+
1167
+ if (typeof productId === 'string' && productId && typeof productController.getProductBySlug === 'function') {
1168
+ ctx.params.slug = productId;
1169
+ await productController.getProductBySlug(ctx);
1170
+ return;
1171
+ }
1172
+ }
1173
+
1174
+ if (method === 'post' && productAction === 'bulk' && typeof productController.createBulkProducts === 'function') {
1175
+ await productController.createBulkProducts(ctx);
1176
+ return;
1177
+ }
1178
+
1179
+ if (method === 'post' && !productId && !productAction && typeof productController.createProduct === 'function') {
1180
+ await productController.createProduct(ctx);
1181
+ return;
1182
+ }
1183
+
1184
+ if (method === 'put' && productId && typeof productController.updateProduct === 'function') {
1185
+ await productController.updateProduct(ctx);
1186
+ return;
1187
+ }
1188
+
1189
+ if (method === 'delete' && productId && typeof productController.deleteProduct === 'function') {
1190
+ await productController.deleteProduct(ctx);
1191
+ return;
1192
+ }
1193
+ }
1194
+ }
1195
+
1196
+ // Handle cart routes
1197
+ // Check both custom prefix and default prefix (after rewrite)
1198
+ const customCartPath = `/api/${routePrefix}/cart`;
1199
+ const defaultCartPath = `/api/webbycommerce/cart`;
1200
+ const isCartRoute =
1201
+ ctx.path === customCartPath ||
1202
+ ctx.path.startsWith(`${customCartPath}/`) ||
1203
+ ctx.path === defaultCartPath ||
1204
+ ctx.path.startsWith(`${defaultCartPath}/`) ||
1205
+ originalPath === customCartPath ||
1206
+ originalPath.startsWith(`${customCartPath}/`);
1207
+
1208
+ // Debug logging for all cart-related requests
1209
+ if (ctx.path.includes('/cart/')) {
1210
+ strapi.log.debug(`[webbycommerce] Cart route detected:`, {
1211
+ path: ctx.path,
1212
+ originalPath,
1213
+ method: ctx.method,
1214
+ routePrefix
1215
+ });
1216
+ }
1217
+
1218
+ // Exclude special cart routes that should be handled by normal Strapi routing
1219
+ // Note: apply-coupon and coupon routes are now handled by custom middleware for proper authentication
1220
+ const isSpecialCartRoute = false; // Temporarily disable special route exclusion
1221
+
1222
+ if (ctx.path.includes('/cart/')) {
1223
+ strapi.log.debug(`[webbycommerce] Cart route analysis:`, {
1224
+ isCartRoute,
1225
+ isSpecialCartRoute,
1226
+ willIntercept: isCartRoute && !isSpecialCartRoute
1227
+ });
1228
+ }
1229
+
1230
+ if (isCartRoute && !isSpecialCartRoute) {
1231
+ // Determine cart sub-route/action and cart item id safely
1232
+ // Supported:
1233
+ // GET /cart -> get cart
1234
+ // POST /cart -> add item
1235
+ // PUT /cart/:id -> update item qty
1236
+ // DELETE /cart/:id -> remove item
1237
+ // DELETE /cart -> clear cart
1238
+ // GET /cart/totals -> totals
1239
+ // POST /cart/create -> create/get cart
1240
+ // POST /cart/checkout -> mark ordered
1241
+ // POST /cart/apply-coupon -> apply coupon (legacy)
1242
+ // DELETE /cart/coupon -> remove coupon (legacy)
1243
+ const pathParts = (ctx.path || '').split('/').filter(Boolean);
1244
+ const cartIndex = pathParts.lastIndexOf('cart');
1245
+ const cartNext = cartIndex >= 0 ? pathParts[cartIndex + 1] : null;
1246
+
1247
+ const reserved = new Set(['apply-coupon', 'coupon', 'totals', 'create', 'checkout']);
1248
+ const isNumericId = (value) => typeof value === 'string' && /^[0-9]+$/.test(value);
1249
+
1250
+ const cartAction = cartNext && reserved.has(cartNext) ? cartNext : null;
1251
+ const cartItemId = cartNext && !cartAction && isNumericId(cartNext) ? cartNext : null;
1252
+
1253
+ // Set ctx.params.id for controllers expecting :id
1254
+ if (cartItemId) {
1255
+ if (!ctx.params) {
1256
+ ctx.params = {};
1257
+ }
1258
+ ctx.params.id = cartItemId;
1259
+ }
1260
+
1261
+ // Authenticate user for cart routes
1262
+ const authHeader = ctx.request.header.authorization;
1263
+ if (authHeader && authHeader.startsWith('Bearer ')) {
1264
+ const token = authHeader.replace('Bearer ', '').trim();
1265
+ if (token) {
1266
+ try {
1267
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
1268
+ if (jwtService && typeof jwtService.verify === 'function') {
1269
+ const decoded = await jwtService.verify(token);
1270
+
1271
+ if (decoded && decoded.id) {
1272
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
1273
+ where: { id: decoded.id },
1274
+ populate: ['role'],
1275
+ });
1276
+
1277
+ if (user) {
1278
+ ctx.state.user = user;
1279
+ }
1280
+ }
1281
+ }
1282
+ } catch (error) {
1283
+ // JWT verification failure is expected for guest carts - log at debug level
1284
+ strapi.log.debug(`[webbycommerce] JWT verification failed for cart route:`, error.message);
1285
+ }
1286
+ }
1287
+ }
1288
+
1289
+ // Check ecommerce permission
1290
+ const hasPermission = await ensureEcommercePermission(ctx);
1291
+ if (!hasPermission) {
1292
+ return;
1293
+ }
1294
+
1295
+ // Parse request body for POST/PUT requests if not already parsed
1296
+ const method = ctx.method.toLowerCase();
1297
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
1298
+ try {
1299
+ // Read and parse JSON body manually since we're intercepting before body parser
1300
+ const contentType = ctx.request.header['content-type'] || '';
1301
+
1302
+ if (contentType.includes('application/json')) {
1303
+ // Read raw body from request stream
1304
+ const chunks = [];
1305
+ for await (const chunk of ctx.req) {
1306
+ chunks.push(chunk);
1307
+ }
1308
+ const rawBody = Buffer.concat(chunks).toString('utf8');
1309
+
1310
+ if (rawBody && rawBody.trim()) {
1311
+ ctx.request.body = JSON.parse(rawBody);
1312
+ strapi.log.debug(`[webbycommerce] Parsed request body for cart:`, ctx.request.body);
1313
+ }
1314
+ }
1315
+ } catch (error) {
1316
+ strapi.log.error(`[webbycommerce] Failed to parse request body for cart:`, error.message);
1317
+ // Continue - controller will handle validation errors
1318
+ }
1319
+ }
1320
+
1321
+ const cartController = strapi
1322
+ .plugin('webbycommerce')
1323
+ .controller('cart');
1324
+
1325
+ if (cartController) {
1326
+ // Special cart routes first
1327
+ if (method === 'get' && cartAction === 'totals' && typeof cartController.getTotals === 'function') {
1328
+ await cartController.getTotals(ctx);
1329
+ return;
1330
+ }
1331
+
1332
+ if (method === 'post' && cartAction === 'create' && typeof cartController.createCart === 'function') {
1333
+ await cartController.createCart(ctx);
1334
+ return;
1335
+ }
1336
+
1337
+ if (method === 'post' && cartAction === 'checkout' && typeof cartController.checkout === 'function') {
1338
+ await cartController.checkout(ctx);
1339
+ return;
1340
+ }
1341
+
1342
+ // Legacy coupon endpoints (kept for compatibility)
1343
+ if (method === 'post' && cartAction === 'apply-coupon' && typeof cartController.applyCoupon === 'function') {
1344
+ await cartController.applyCoupon(ctx);
1345
+ return;
1346
+ }
1347
+
1348
+ if (method === 'delete' && cartAction === 'coupon' && typeof cartController.removeCoupon === 'function') {
1349
+ await cartController.removeCoupon(ctx);
1350
+ return;
1351
+ }
1352
+
1353
+ // Base cart routes
1354
+ if (method === 'get' && !cartAction && !cartItemId && typeof cartController.getCart === 'function') {
1355
+ await cartController.getCart(ctx);
1356
+ return;
1357
+ }
1358
+
1359
+ if (method === 'get' && !cartAction && !cartItemId && typeof cartController.getItems === 'function') {
1360
+ await cartController.getItems(ctx);
1361
+ return;
1362
+ }
1363
+
1364
+ if (method === 'post' && !cartAction && !cartItemId && typeof cartController.addItem === 'function') {
1365
+ await cartController.addItem(ctx);
1366
+ return;
1367
+ }
1368
+
1369
+ if (method === 'put' && cartItemId && typeof cartController.updateItem === 'function') {
1370
+ await cartController.updateItem(ctx);
1371
+ return;
1372
+ }
1373
+
1374
+ if (method === 'delete' && cartItemId && typeof cartController.removeItem === 'function') {
1375
+ await cartController.removeItem(ctx);
1376
+ return;
1377
+ }
1378
+
1379
+ if (method === 'delete' && !cartAction && !cartItemId && typeof cartController.clearCart === 'function') {
1380
+ await cartController.clearCart(ctx);
1381
+ return;
1382
+ }
1383
+
1384
+ }
1385
+ }
1386
+
1387
+ // Handle product-variant routes
1388
+ const customProductVariantPath = `/api/${routePrefix}/product-variants`;
1389
+ const defaultProductVariantPath = `/api/webbycommerce/product-variants`;
1390
+ const isProductVariantRoute =
1391
+ ctx.path === customProductVariantPath ||
1392
+ ctx.path.startsWith(`${customProductVariantPath}/`) ||
1393
+ ctx.path === defaultProductVariantPath ||
1394
+ ctx.path.startsWith(`${defaultProductVariantPath}/`) ||
1395
+ originalPath === customProductVariantPath ||
1396
+ originalPath.startsWith(`${customProductVariantPath}/`);
1397
+
1398
+ if (isProductVariantRoute) {
1399
+ let productVariantId = null;
1400
+ const pathMatch = ctx.path.match(/\/product-variants\/([^\/]+)/);
1401
+ if (pathMatch) {
1402
+ productVariantId = pathMatch[1];
1403
+ if (!ctx.params) {
1404
+ ctx.params = {};
1405
+ }
1406
+ ctx.params.id = productVariantId;
1407
+ }
1408
+
1409
+ // Authenticate user for product-variant routes (optional for public endpoints)
1410
+ const authHeader = ctx.request.header.authorization;
1411
+ if (authHeader && authHeader.startsWith('Bearer ')) {
1412
+ const token = authHeader.replace('Bearer ', '').trim();
1413
+ if (token) {
1414
+ try {
1415
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
1416
+ if (jwtService && typeof jwtService.verify === 'function') {
1417
+ const decoded = await jwtService.verify(token);
1418
+
1419
+ if (decoded && decoded.id) {
1420
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
1421
+ where: { id: decoded.id },
1422
+ populate: ['role'],
1423
+ });
1424
+
1425
+ if (user) {
1426
+ ctx.state.user = user;
1427
+ }
1428
+ }
1429
+ }
1430
+ } catch (error) {
1431
+ // JWT verification failure is expected for public endpoints - log at debug level
1432
+ strapi.log.debug(`[webbycommerce] JWT verification failed for product-variant route:`, error.message);
1433
+ }
1434
+ }
1435
+ }
1436
+
1437
+ // Check ecommerce permission
1438
+ const hasPermissionForProductVariants = await ensureEcommercePermission(ctx);
1439
+ if (!hasPermissionForProductVariants) {
1440
+ return;
1441
+ }
1442
+
1443
+ // Parse request body for POST/PUT requests if not already parsed
1444
+ const method = ctx.method.toLowerCase();
1445
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
1446
+ try {
1447
+ const contentType = ctx.request.header['content-type'] || '';
1448
+
1449
+ if (contentType.includes('application/json')) {
1450
+ const chunks = [];
1451
+ for await (const chunk of ctx.req) {
1452
+ chunks.push(chunk);
1453
+ }
1454
+ const rawBody = Buffer.concat(chunks).toString('utf8');
1455
+
1456
+ if (rawBody && rawBody.trim()) {
1457
+ ctx.request.body = JSON.parse(rawBody);
1458
+ strapi.log.debug(`[webbycommerce] Parsed request body for product-variants:`, ctx.request.body);
1459
+ }
1460
+ }
1461
+ } catch (error) {
1462
+ strapi.log.error(`[webbycommerce] Failed to parse request body for product-variant route:`, error.message);
1463
+ }
1464
+ }
1465
+
1466
+ const productVariantController = strapi.plugin('webbycommerce').controller('productVariant');
1467
+
1468
+ if (productVariantController) {
1469
+ if (method === 'get' && !productVariantId && typeof productVariantController.getProductVariants === 'function') {
1470
+ await productVariantController.getProductVariants(ctx);
1471
+ return;
1472
+ }
1473
+
1474
+ if (method === 'get' && productVariantId && typeof productVariantController.getProductVariant === 'function') {
1475
+ await productVariantController.getProductVariant(ctx);
1476
+ return;
1477
+ }
1478
+
1479
+ if (method === 'post' && typeof productVariantController.createProductVariant === 'function') {
1480
+ await productVariantController.createProductVariant(ctx);
1481
+ return;
1482
+ }
1483
+
1484
+ if (method === 'put' && productVariantId && typeof productVariantController.updateProductVariant === 'function') {
1485
+ await productVariantController.updateProductVariant(ctx);
1486
+ return;
1487
+ }
1488
+
1489
+ if (method === 'delete' && productVariantId && typeof productVariantController.deleteProductVariant === 'function') {
1490
+ await productVariantController.deleteProductVariant(ctx);
1491
+ return;
1492
+ }
1493
+ }
1494
+ }
1495
+
1496
+ // Handle product-attribute routes
1497
+ const customProductAttributePath = `/api/${routePrefix}/product-attributes`;
1498
+ const defaultProductAttributePath = `/api/webbycommerce/product-attributes`;
1499
+ const isProductAttributeRoute =
1500
+ ctx.path === customProductAttributePath ||
1501
+ ctx.path.startsWith(`${customProductAttributePath}/`) ||
1502
+ ctx.path === defaultProductAttributePath ||
1503
+ ctx.path.startsWith(`${defaultProductAttributePath}/`) ||
1504
+ originalPath === customProductAttributePath ||
1505
+ originalPath.startsWith(`${customProductAttributePath}/`);
1506
+
1507
+ if (isProductAttributeRoute) {
1508
+ let productAttributeId = null;
1509
+ const pathMatch = ctx.path.match(/\/product-attributes\/([^\/]+)/);
1510
+ if (pathMatch) {
1511
+ productAttributeId = pathMatch[1];
1512
+ if (!ctx.params) {
1513
+ ctx.params = {};
1514
+ }
1515
+ ctx.params.id = productAttributeId;
1516
+ }
1517
+
1518
+ // Authenticate user for product-attribute routes (optional for public endpoints)
1519
+ const authHeader = ctx.request.header.authorization;
1520
+ if (authHeader && authHeader.startsWith('Bearer ')) {
1521
+ const token = authHeader.replace('Bearer ', '').trim();
1522
+ if (token) {
1523
+ try {
1524
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
1525
+ if (jwtService && typeof jwtService.verify === 'function') {
1526
+ const decoded = await jwtService.verify(token);
1527
+
1528
+ if (decoded && decoded.id) {
1529
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
1530
+ where: { id: decoded.id },
1531
+ populate: ['role'],
1532
+ });
1533
+
1534
+ if (user) {
1535
+ ctx.state.user = user;
1536
+ }
1537
+ }
1538
+ }
1539
+ } catch (error) {
1540
+ // JWT verification failure is expected for public endpoints - log at debug level
1541
+ strapi.log.debug(`[webbycommerce] JWT verification failed for product-attribute route:`, error.message);
1542
+ }
1543
+ }
1544
+ }
1545
+
1546
+ // Check ecommerce permission
1547
+ const hasPermissionForProductAttributes = await ensureEcommercePermission(ctx);
1548
+ if (!hasPermissionForProductAttributes) {
1549
+ return;
1550
+ }
1551
+
1552
+ // Parse request body for POST/PUT requests if not already parsed
1553
+ const method = ctx.method.toLowerCase();
1554
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
1555
+ try {
1556
+ const contentType = ctx.request.header['content-type'] || '';
1557
+
1558
+ if (contentType.includes('application/json')) {
1559
+ const chunks = [];
1560
+ for await (const chunk of ctx.req) {
1561
+ chunks.push(chunk);
1562
+ }
1563
+ const rawBody = Buffer.concat(chunks).toString('utf8');
1564
+
1565
+ if (rawBody && rawBody.trim()) {
1566
+ ctx.request.body = JSON.parse(rawBody);
1567
+ strapi.log.debug(`[webbycommerce] Parsed request body for product-attributes:`, ctx.request.body);
1568
+ }
1569
+ }
1570
+ } catch (error) {
1571
+ strapi.log.error(`[webbycommerce] Failed to parse request body for product-attribute route:`, error.message);
1572
+ }
1573
+ }
1574
+
1575
+ // Route to Strapi's content API handlers
1576
+ if (method === 'get' && !productAttributeId) {
1577
+ // GET /product-attributes - find many
1578
+ const entities = await strapi.entityService.findMany('plugin::webbycommerce.product-attribute', {
1579
+ sort: { sort_order: 'asc' },
1580
+ populate: ['product_attribute_values'],
1581
+ });
1582
+ ctx.send({ data: entities });
1583
+ return;
1584
+ }
1585
+
1586
+ if (method === 'get' && productAttributeId) {
1587
+ // GET /product-attributes/:id - find one (supports both ID and slug)
1588
+ const isNumericId = /^[0-9]+$/.test(productAttributeId);
1589
+ let entity;
1590
+
1591
+ if (isNumericId) {
1592
+ // Query by ID
1593
+ entity = await strapi.entityService.findOne('plugin::webbycommerce.product-attribute', productAttributeId, {
1594
+ populate: ['product_attribute_values'],
1595
+ });
1596
+ } else {
1597
+ // Query by slug
1598
+ const decodedSlug = decodeURIComponent(productAttributeId).trim();
1599
+ const results = await strapi.db.query('plugin::webbycommerce.product-attribute').findMany({
1600
+ where: { slug: decodedSlug },
1601
+ limit: 1,
1602
+ orderBy: { id: 'desc' },
1603
+ populate: ['product_attribute_values'],
1604
+ });
1605
+ entity = results?.[0];
1606
+ }
1607
+
1608
+ if (!entity) {
1609
+ return ctx.notFound('Product attribute not found');
1610
+ }
1611
+ ctx.send({ data: entity });
1612
+ return;
1613
+ }
1614
+
1615
+ if (method === 'post') {
1616
+ // POST /product-attributes - create
1617
+ const entity = await strapi.entityService.create('plugin::webbycommerce.product-attribute', {
1618
+ data: ctx.request.body,
1619
+ });
1620
+ ctx.send({ data: entity });
1621
+ return;
1622
+ }
1623
+
1624
+ if (method === 'put' && productAttributeId) {
1625
+ // PUT /product-attributes/:id - update
1626
+ const entity = await strapi.entityService.update('plugin::webbycommerce.product-attribute', productAttributeId, {
1627
+ data: ctx.request.body,
1628
+ });
1629
+ if (!entity) {
1630
+ return ctx.notFound('Product attribute not found');
1631
+ }
1632
+ ctx.send({ data: entity });
1633
+ return;
1634
+ }
1635
+
1636
+ if (method === 'delete' && productAttributeId) {
1637
+ // DELETE /product-attributes/:id - delete
1638
+ const entity = await strapi.entityService.delete('plugin::webbycommerce.product-attribute', productAttributeId);
1639
+ if (!entity) {
1640
+ return ctx.notFound('Product attribute not found');
1641
+ }
1642
+ ctx.send({ data: entity });
1643
+ return;
1644
+ }
1645
+ }
1646
+
1647
+ // Handle product-attribute-value routes
1648
+ const customProductAttributeValuePath = `/api/${routePrefix}/product-attribute-values`;
1649
+ const defaultProductAttributeValuePath = `/api/webbycommerce/product-attribute-values`;
1650
+ const isProductAttributeValueRoute =
1651
+ ctx.path === customProductAttributeValuePath ||
1652
+ ctx.path.startsWith(`${customProductAttributeValuePath}/`) ||
1653
+ ctx.path === defaultProductAttributeValuePath ||
1654
+ ctx.path.startsWith(`${defaultProductAttributeValuePath}/`) ||
1655
+ originalPath === customProductAttributeValuePath ||
1656
+ originalPath.startsWith(`${customProductAttributeValuePath}/`);
1657
+
1658
+ if (isProductAttributeValueRoute) {
1659
+ let productAttributeValueId = null;
1660
+ const pathMatch = ctx.path.match(/\/product-attribute-values\/([^\/]+)/);
1661
+ if (pathMatch) {
1662
+ productAttributeValueId = pathMatch[1];
1663
+ if (!ctx.params) {
1664
+ ctx.params = {};
1665
+ }
1666
+ ctx.params.id = productAttributeValueId;
1667
+ }
1668
+
1669
+ // Authenticate user for product-attribute-value routes (optional for public endpoints)
1670
+ const authHeader = ctx.request.header.authorization;
1671
+ if (authHeader && authHeader.startsWith('Bearer ')) {
1672
+ const token = authHeader.replace('Bearer ', '').trim();
1673
+ if (token) {
1674
+ try {
1675
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
1676
+ if (jwtService && typeof jwtService.verify === 'function') {
1677
+ const decoded = await jwtService.verify(token);
1678
+
1679
+ if (decoded && decoded.id) {
1680
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
1681
+ where: { id: decoded.id },
1682
+ populate: ['role'],
1683
+ });
1684
+
1685
+ if (user) {
1686
+ ctx.state.user = user;
1687
+ }
1688
+ }
1689
+ }
1690
+ } catch (error) {
1691
+ // JWT verification failure is expected for public endpoints - log at debug level
1692
+ strapi.log.debug(`[webbycommerce] JWT verification failed for product-attribute-value route:`, error.message);
1693
+ }
1694
+ }
1695
+ }
1696
+
1697
+ // Check ecommerce permission
1698
+ const hasPermissionForProductAttributeValues = await ensureEcommercePermission(ctx);
1699
+ if (!hasPermissionForProductAttributeValues) {
1700
+ return;
1701
+ }
1702
+
1703
+ // Parse request body for POST/PUT requests if not already parsed
1704
+ const method = ctx.method.toLowerCase();
1705
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
1706
+ try {
1707
+ const contentType = ctx.request.header['content-type'] || '';
1708
+
1709
+ if (contentType.includes('application/json')) {
1710
+ const chunks = [];
1711
+ for await (const chunk of ctx.req) {
1712
+ chunks.push(chunk);
1713
+ }
1714
+ const rawBody = Buffer.concat(chunks).toString('utf8');
1715
+
1716
+ if (rawBody && rawBody.trim()) {
1717
+ ctx.request.body = JSON.parse(rawBody);
1718
+ strapi.log.debug(`[webbycommerce] Parsed request body for product-attribute-values:`, ctx.request.body);
1719
+ }
1720
+ }
1721
+ } catch (error) {
1722
+ strapi.log.error(`[webbycommerce] Failed to parse request body for product-attribute-value route:`, error.message);
1723
+ }
1724
+ }
1725
+
1726
+ // Route to Strapi's content API handlers
1727
+ if (method === 'get' && !productAttributeValueId) {
1728
+ // GET /product-attribute-values - find many
1729
+ const entities = await strapi.entityService.findMany('plugin::webbycommerce.product-attribute-value', {
1730
+ sort: { sort_order: 'asc' },
1731
+ populate: ['product_attribute'],
1732
+ });
1733
+ ctx.send({ data: entities });
1734
+ return;
1735
+ }
1736
+
1737
+ if (method === 'get' && productAttributeValueId) {
1738
+ // GET /product-attribute-values/:id - find one (supports both ID and slug)
1739
+ const isNumericId = /^[0-9]+$/.test(productAttributeValueId);
1740
+ let entity;
1741
+
1742
+ if (isNumericId) {
1743
+ // Query by ID
1744
+ entity = await strapi.entityService.findOne('plugin::webbycommerce.product-attribute-value', productAttributeValueId, {
1745
+ populate: ['product_attribute'],
1746
+ });
1747
+ } else {
1748
+ // Query by slug
1749
+ const decodedSlug = decodeURIComponent(productAttributeValueId).trim();
1750
+ const results = await strapi.db.query('plugin::webbycommerce.product-attribute-value').findMany({
1751
+ where: { slug: decodedSlug },
1752
+ limit: 1,
1753
+ orderBy: { id: 'desc' },
1754
+ populate: ['product_attribute'],
1755
+ });
1756
+ entity = results?.[0];
1757
+ }
1758
+
1759
+ if (!entity) {
1760
+ return ctx.notFound('Product attribute value not found');
1761
+ }
1762
+ ctx.send({ data: entity });
1763
+ return;
1764
+ }
1765
+
1766
+ if (method === 'post') {
1767
+ // POST /product-attribute-values - create
1768
+ const entity = await strapi.entityService.create('plugin::webbycommerce.product-attribute-value', {
1769
+ data: ctx.request.body,
1770
+ });
1771
+ ctx.send({ data: entity });
1772
+ return;
1773
+ }
1774
+
1775
+ if (method === 'put' && productAttributeValueId) {
1776
+ // PUT /product-attribute-values/:id - update
1777
+ const entity = await strapi.entityService.update('plugin::webbycommerce.product-attribute-value', productAttributeValueId, {
1778
+ data: ctx.request.body,
1779
+ });
1780
+ if (!entity) {
1781
+ return ctx.notFound('Product attribute value not found');
1782
+ }
1783
+ ctx.send({ data: entity });
1784
+ return;
1785
+ }
1786
+
1787
+ if (method === 'delete' && productAttributeValueId) {
1788
+ // DELETE /product-attribute-values/:id - delete
1789
+ const entity = await strapi.entityService.delete('plugin::webbycommerce.product-attribute-value', productAttributeValueId);
1790
+ if (!entity) {
1791
+ return ctx.notFound('Product attribute value not found');
1792
+ }
1793
+ ctx.send({ data: entity });
1794
+ return;
1795
+ }
1796
+ }
1797
+
1798
+ // Handle wishlist routes
1799
+ // Check both custom prefix and default prefix (after rewrite)
1800
+ const customWishlistPath = `/api/${routePrefix}/wishlist`;
1801
+ const defaultWishlistPath = `/api/webbycommerce/wishlist`;
1802
+ const isWishlistRoute =
1803
+ ctx.path === customWishlistPath ||
1804
+ ctx.path.startsWith(`${customWishlistPath}/`) ||
1805
+ ctx.path === defaultWishlistPath ||
1806
+ ctx.path.startsWith(`${defaultWishlistPath}/`) ||
1807
+ originalPath === customWishlistPath ||
1808
+ originalPath.startsWith(`${customWishlistPath}/`);
1809
+
1810
+ // Check if this is a move-to-cart route
1811
+ const isMoveToCartRoute =
1812
+ ctx.path.includes('/move-to-cart') ||
1813
+ originalPath.includes('/move-to-cart');
1814
+
1815
+ // Exclude special wishlist routes that should be handled by normal Strapi routing
1816
+ // But include move-to-cart routes
1817
+ const isSpecialWishlistRoute =
1818
+ (ctx.path.includes('/wishlist/items/') ||
1819
+ originalPath.includes('/wishlist/items/')) &&
1820
+ !isMoveToCartRoute;
1821
+
1822
+ if (isWishlistRoute && !isSpecialWishlistRoute) {
1823
+ // Extract product ID from path if present (for remove operations)
1824
+ let productId = null;
1825
+ const pathMatch = ctx.path.match(/\/wishlist\/([^\/]+)/);
1826
+ if (pathMatch) {
1827
+ productId = pathMatch[1];
1828
+ // Set ctx.params.productId for controller access
1829
+ if (!ctx.params) {
1830
+ ctx.params = {};
1831
+ }
1832
+ ctx.params.productId = productId;
1833
+ }
1834
+
1835
+ // Authenticate user for wishlist routes
1836
+ const authHeader = ctx.request.header.authorization;
1837
+ if (authHeader && authHeader.startsWith('Bearer ')) {
1838
+ const token = authHeader.replace('Bearer ', '').trim();
1839
+ if (token) {
1840
+ try {
1841
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
1842
+ if (jwtService && typeof jwtService.verify === 'function') {
1843
+ const decoded = await jwtService.verify(token);
1844
+
1845
+ if (decoded && decoded.id) {
1846
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
1847
+ where: { id: decoded.id },
1848
+ populate: ['role'],
1849
+ });
1850
+
1851
+ if (user) {
1852
+ ctx.state.user = user;
1853
+ }
1854
+ }
1855
+ }
1856
+ } catch (error) {
1857
+ // JWT verification failure is expected for guest access - log at debug level
1858
+ strapi.log.debug(`[webbycommerce] JWT verification failed for wishlist route:`, error.message);
1859
+ }
1860
+ }
1861
+ }
1862
+
1863
+ // Check ecommerce permission
1864
+ const hasPermission = await ensureEcommercePermission(ctx);
1865
+ if (!hasPermission) {
1866
+ return;
1867
+ }
1868
+
1869
+ // Parse request body for POST/PUT requests if not already parsed
1870
+ const method = ctx.method.toLowerCase();
1871
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
1872
+ try {
1873
+ const contentType = ctx.request.header['content-type'] || '';
1874
+
1875
+ if (contentType.includes('application/json')) {
1876
+ const chunks = [];
1877
+ for await (const chunk of ctx.req) {
1878
+ chunks.push(chunk);
1879
+ }
1880
+ const rawBody = Buffer.concat(chunks).toString('utf8');
1881
+
1882
+ if (rawBody && rawBody.trim()) {
1883
+ ctx.request.body = JSON.parse(rawBody);
1884
+ strapi.log.debug(`[webbycommerce] Parsed request body for wishlist:`, ctx.request.body);
1885
+ }
1886
+ }
1887
+ } catch (error) {
1888
+ strapi.log.error(`[webbycommerce] Failed to parse request body for wishlist:`, error.message);
1889
+ // Continue - controller will handle validation errors
1890
+ }
1891
+ }
1892
+
1893
+ const wishlistController = strapi
1894
+ .plugin('webbycommerce')
1895
+ .controller('wishlist');
1896
+
1897
+ if (wishlistController) {
1898
+ if (method === 'get' && !productId && typeof wishlistController.getWishlist === 'function') {
1899
+ await wishlistController.getWishlist(ctx);
1900
+ return;
1901
+ }
1902
+
1903
+ if (method === 'post' && !productId && typeof wishlistController.addToWishlist === 'function') {
1904
+ await wishlistController.addToWishlist(ctx);
1905
+ return;
1906
+ }
1907
+
1908
+ if (method === 'delete' && productId && typeof wishlistController.removeFromWishlist === 'function') {
1909
+ await wishlistController.removeFromWishlist(ctx);
1910
+ return;
1911
+ }
1912
+
1913
+ if (method === 'delete' && !productId && typeof wishlistController.clearWishlist === 'function') {
1914
+ await wishlistController.clearWishlist(ctx);
1915
+ return;
1916
+ }
1917
+
1918
+ if (method === 'put' && !productId && typeof wishlistController.updateWishlist === 'function') {
1919
+ await wishlistController.updateWishlist(ctx);
1920
+ return;
1921
+ }
1922
+
1923
+ if (method === 'get' && ctx.path.includes('/status') && typeof wishlistController.checkWishlistStatus === 'function') {
1924
+ await wishlistController.checkWishlistStatus(ctx);
1925
+ return;
1926
+ }
1927
+
1928
+ }
1929
+ }
1930
+
1931
+ // Handle move-to-cart route (special wishlist route)
1932
+ if (isWishlistRoute && isMoveToCartRoute) {
1933
+ // Authenticate user for move-to-cart route
1934
+ const authHeader = ctx.request.header.authorization;
1935
+ if (authHeader && authHeader.startsWith('Bearer ')) {
1936
+ const token = authHeader.replace('Bearer ', '').trim();
1937
+ if (token) {
1938
+ try {
1939
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
1940
+ if (jwtService && typeof jwtService.verify === 'function') {
1941
+ const decoded = await jwtService.verify(token);
1942
+
1943
+ if (decoded && decoded.id) {
1944
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
1945
+ where: { id: decoded.id },
1946
+ populate: ['role'],
1947
+ });
1948
+
1949
+ if (user) {
1950
+ ctx.state.user = user;
1951
+ }
1952
+ }
1953
+ }
1954
+ } catch (error) {
1955
+ // JWT verification failure is expected for guest access - log at debug level
1956
+ strapi.log.debug(`[webbycommerce] JWT verification failed for move-to-cart route:`, error.message);
1957
+ }
1958
+ }
1959
+ }
1960
+
1961
+ // Check ecommerce permission
1962
+ const hasPermission = await ensureEcommercePermission(ctx);
1963
+ if (!hasPermission) {
1964
+ return;
1965
+ }
1966
+
1967
+ // Parse request body for POST requests if not already parsed
1968
+ const method = ctx.method.toLowerCase();
1969
+ if (method === 'post' && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
1970
+ try {
1971
+ const contentType = ctx.request.header['content-type'] || '';
1972
+
1973
+ if (contentType.includes('application/json')) {
1974
+ const chunks = [];
1975
+ for await (const chunk of ctx.req) {
1976
+ chunks.push(chunk);
1977
+ }
1978
+ const rawBody = Buffer.concat(chunks).toString('utf8');
1979
+
1980
+ if (rawBody && rawBody.trim()) {
1981
+ ctx.request.body = JSON.parse(rawBody);
1982
+ strapi.log.debug(`[webbycommerce] Parsed request body for move-to-cart:`, ctx.request.body);
1983
+ }
1984
+ }
1985
+ } catch (error) {
1986
+ strapi.log.error(`[webbycommerce] Failed to parse request body for move-to-cart:`, error.message);
1987
+ // Continue - controller will handle validation errors
1988
+ }
1989
+ }
1990
+
1991
+ const wishlistController = strapi
1992
+ .plugin('webbycommerce')
1993
+ .controller('wishlist');
1994
+
1995
+ if (wishlistController && method === 'post' && typeof wishlistController.moveToCart === 'function') {
1996
+ // Extract ID from path for move-to-cart
1997
+ const moveToCartMatch = ctx.path.match(/\/wishlist\/items\/([^\/]+)\/move-to-cart/);
1998
+ if (moveToCartMatch) {
1999
+ if (!ctx.params) {
2000
+ ctx.params = {};
2001
+ }
2002
+ ctx.params.id = moveToCartMatch[1];
2003
+ }
2004
+ await wishlistController.moveToCart(ctx);
2005
+ return;
2006
+ }
2007
+ }
2008
+
2009
+ // Handle compare routes
2010
+ // Check both custom prefix and default prefix (after rewrite)
2011
+ const customComparePath = `/api/${routePrefix}/compare`;
2012
+ const defaultComparePath = `/api/webbycommerce/compare`;
2013
+ const isCompareRoute =
2014
+ ctx.path === customComparePath ||
2015
+ ctx.path.startsWith(`${customComparePath}/`) ||
2016
+ ctx.path === defaultComparePath ||
2017
+ ctx.path.startsWith(`${defaultComparePath}/`) ||
2018
+ originalPath === customComparePath ||
2019
+ originalPath.startsWith(`${customComparePath}/`);
2020
+
2021
+ if (isCompareRoute) {
2022
+ // Extract product ID from path if present (for remove operations)
2023
+ let productId = null;
2024
+ const pathMatch = ctx.path.match(/\/compare\/([^\/]+)/);
2025
+ if (pathMatch) {
2026
+ productId = pathMatch[1];
2027
+ // Set ctx.params.productId for controller access
2028
+ if (!ctx.params) {
2029
+ ctx.params = {};
2030
+ }
2031
+ ctx.params.productId = productId;
2032
+ }
2033
+
2034
+ // Authenticate user for compare routes
2035
+ const authHeader = ctx.request.header.authorization;
2036
+ if (authHeader && authHeader.startsWith('Bearer ')) {
2037
+ const token = authHeader.replace('Bearer ', '').trim();
2038
+ if (token) {
2039
+ try {
2040
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
2041
+ if (jwtService && typeof jwtService.verify === 'function') {
2042
+ const decoded = await jwtService.verify(token);
2043
+
2044
+ if (decoded && decoded.id) {
2045
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
2046
+ where: { id: decoded.id },
2047
+ populate: ['role'],
2048
+ });
2049
+
2050
+ if (user) {
2051
+ ctx.state.user = user;
2052
+ }
2053
+ }
2054
+ }
2055
+ } catch (error) {
2056
+ // JWT verification failure is expected for guest access - log at debug level
2057
+ strapi.log.debug(`[webbycommerce] JWT verification failed for compare route:`, error.message);
2058
+ }
2059
+ }
2060
+ }
2061
+
2062
+ // Check ecommerce permission
2063
+ const hasPermission = await ensureEcommercePermission(ctx);
2064
+ if (!hasPermission) {
2065
+ return;
2066
+ }
2067
+
2068
+ // Parse request body for POST/PUT requests if not already parsed
2069
+ const method = ctx.method.toLowerCase();
2070
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
2071
+ try {
2072
+ const contentType = ctx.request.header['content-type'] || '';
2073
+
2074
+ if (contentType.includes('application/json')) {
2075
+ const chunks = [];
2076
+ for await (const chunk of ctx.req) {
2077
+ chunks.push(chunk);
2078
+ }
2079
+ const rawBody = Buffer.concat(chunks).toString('utf8');
2080
+
2081
+ if (rawBody && rawBody.trim()) {
2082
+ ctx.request.body = JSON.parse(rawBody);
2083
+ strapi.log.debug(`[webbycommerce] Parsed request body for compare:`, ctx.request.body);
2084
+ }
2085
+ }
2086
+ } catch (error) {
2087
+ strapi.log.error(`[webbycommerce] Failed to parse request body for compare:`, error.message);
2088
+ // Continue - controller will handle validation errors
2089
+ }
2090
+ }
2091
+
2092
+ const compareController = strapi
2093
+ .plugin('webbycommerce')
2094
+ .controller('compare');
2095
+
2096
+ if (compareController) {
2097
+ if (method === 'get' && !productId && !ctx.path.includes('/data') && !ctx.path.includes('/status') && typeof compareController.getCompare === 'function') {
2098
+ await compareController.getCompare(ctx);
2099
+ return;
2100
+ }
2101
+
2102
+ if (method === 'post' && !productId && typeof compareController.addToCompare === 'function') {
2103
+ await compareController.addToCompare(ctx);
2104
+ return;
2105
+ }
2106
+
2107
+ if (method === 'delete' && productId && typeof compareController.removeFromCompare === 'function') {
2108
+ await compareController.removeFromCompare(ctx);
2109
+ return;
2110
+ }
2111
+
2112
+ if (method === 'delete' && !productId && typeof compareController.clearCompare === 'function') {
2113
+ await compareController.clearCompare(ctx);
2114
+ return;
2115
+ }
2116
+
2117
+ if (method === 'put' && !productId && typeof compareController.updateCompare === 'function') {
2118
+ await compareController.updateCompare(ctx);
2119
+ return;
2120
+ }
2121
+
2122
+ if (method === 'get' && ctx.path.includes('/data') && typeof compareController.getComparisonData === 'function') {
2123
+ await compareController.getComparisonData(ctx);
2124
+ return;
2125
+ }
2126
+
2127
+ if (method === 'get' && ctx.path.includes('/status') && typeof compareController.checkCompareStatus === 'function') {
2128
+ await compareController.checkCompareStatus(ctx);
2129
+ return;
2130
+ }
2131
+ }
2132
+ }
2133
+
2134
+ // Handle tag routes
2135
+ // Handle product-category routes
2136
+ const customProductCategoryPath = `/api/${routePrefix}/product-categories`;
2137
+ const defaultProductCategoryPath = `/api/webbycommerce/product-categories`;
2138
+ const isProductCategoryRoute =
2139
+ ctx.path === customProductCategoryPath ||
2140
+ ctx.path.startsWith(`${customProductCategoryPath}/`) ||
2141
+ ctx.path === defaultProductCategoryPath ||
2142
+ ctx.path.startsWith(`${defaultProductCategoryPath}/`) ||
2143
+ originalPath === customProductCategoryPath ||
2144
+ originalPath.startsWith(`${customProductCategoryPath}/`);
2145
+
2146
+ if (isProductCategoryRoute) {
2147
+ let productCategoryId = null;
2148
+ const pathMatchCat = ctx.path.match(/\/product-categories\/([^\/]+)/);
2149
+ if (pathMatchCat) {
2150
+ productCategoryId = pathMatchCat[1];
2151
+ if (!ctx.params) {
2152
+ ctx.params = {};
2153
+ }
2154
+ ctx.params.id = productCategoryId;
2155
+ }
2156
+
2157
+ // Authenticate user for product-category routes (optional for public endpoints)
2158
+ const authHeaderCat = ctx.request.header.authorization;
2159
+ if (authHeaderCat && authHeaderCat.startsWith('Bearer ')) {
2160
+ const token = authHeaderCat.replace('Bearer ', '').trim();
2161
+ if (token) {
2162
+ try {
2163
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
2164
+ if (jwtService && typeof jwtService.verify === 'function') {
2165
+ const decoded = await jwtService.verify(token);
2166
+
2167
+ if (decoded && decoded.id) {
2168
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
2169
+ where: { id: decoded.id },
2170
+ populate: ['role'],
2171
+ });
2172
+
2173
+ if (user) {
2174
+ ctx.state.user = user;
2175
+ }
2176
+ }
2177
+ }
2178
+ } catch (error) {
2179
+ // JWT verification failure is expected for public endpoints - log at debug level
2180
+ strapi.log.debug(`[webbycommerce] JWT verification failed for product-category route:`, error.message);
2181
+ }
2182
+ }
2183
+ }
2184
+
2185
+ // Check ecommerce permission
2186
+ const hasPermissionForProductCategories = await ensureEcommercePermission(ctx);
2187
+ if (!hasPermissionForProductCategories) {
2188
+ return;
2189
+ }
2190
+
2191
+ // Parse request body for POST/PUT requests if not already parsed
2192
+ const methodCat = ctx.method.toLowerCase();
2193
+ if ((methodCat === 'post' || methodCat === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
2194
+ try {
2195
+ const contentType = ctx.request.header['content-type'] || '';
2196
+
2197
+ if (contentType.includes('application/json')) {
2198
+ const chunks = [];
2199
+ for await (const chunk of ctx.req) {
2200
+ chunks.push(chunk);
2201
+ }
2202
+ const rawBody = Buffer.concat(chunks).toString('utf8');
2203
+
2204
+ if (rawBody && rawBody.trim()) {
2205
+ ctx.request.body = JSON.parse(rawBody);
2206
+ strapi.log.debug(`[webbycommerce] Parsed request body for product-categories:`, ctx.request.body);
2207
+ }
2208
+ }
2209
+ } catch (error) {
2210
+ strapi.log.error(`[webbycommerce] Failed to parse request body for product-category route:`, error.message);
2211
+ }
2212
+ }
2213
+
2214
+ const productCategoryController = strapi.plugin('webbycommerce').controller('productCategory');
2215
+
2216
+ if (productCategoryController) {
2217
+ if (methodCat === 'get' && !productCategoryId && typeof productCategoryController.getProductCategories === 'function') {
2218
+ await productCategoryController.getProductCategories(ctx);
2219
+ return;
2220
+ }
2221
+
2222
+ if (methodCat === 'get' && productCategoryId && typeof productCategoryController.getProductCategory === 'function') {
2223
+ await productCategoryController.getProductCategory(ctx);
2224
+ return;
2225
+ }
2226
+
2227
+ if (methodCat === 'post' && typeof productCategoryController.createProductCategory === 'function') {
2228
+ await productCategoryController.createProductCategory(ctx);
2229
+ return;
2230
+ }
2231
+
2232
+ if (methodCat === 'put' && productCategoryId && typeof productCategoryController.updateProductCategory === 'function') {
2233
+ await productCategoryController.updateProductCategory(ctx);
2234
+ return;
2235
+ }
2236
+
2237
+ if (methodCat === 'delete' && productCategoryId && typeof productCategoryController.deleteProductCategory === 'function') {
2238
+ await productCategoryController.deleteProductCategory(ctx);
2239
+ return;
2240
+ }
2241
+ }
2242
+ }
2243
+
2244
+ const customTagPath = `/api/${routePrefix}/tags`;
2245
+ const defaultTagPath = `/api/webbycommerce/tags`;
2246
+ const isTagRoute =
2247
+ ctx.path === customTagPath ||
2248
+ ctx.path.startsWith(`${customTagPath}/`) ||
2249
+ ctx.path === defaultTagPath ||
2250
+ ctx.path.startsWith(`${defaultTagPath}/`) ||
2251
+ originalPath === customTagPath ||
2252
+ originalPath.startsWith(`${customTagPath}/`);
2253
+
2254
+ if (isTagRoute) {
2255
+ let tagId = null;
2256
+ const pathMatch = ctx.path.match(/\/tags\/([^\/]+)/);
2257
+ if (pathMatch) {
2258
+ tagId = pathMatch[1];
2259
+ if (!ctx.params) {
2260
+ ctx.params = {};
2261
+ }
2262
+ ctx.params.id = tagId;
2263
+ }
2264
+
2265
+ // Authenticate user for tag routes (optional for public endpoints)
2266
+ const authHeader = ctx.request.header.authorization;
2267
+ if (authHeader && authHeader.startsWith('Bearer ')) {
2268
+ const token = authHeader.replace('Bearer ', '').trim();
2269
+ if (token) {
2270
+ try {
2271
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
2272
+ if (jwtService && typeof jwtService.verify === 'function') {
2273
+ const decoded = await jwtService.verify(token);
2274
+
2275
+ if (decoded && decoded.id) {
2276
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
2277
+ where: { id: decoded.id },
2278
+ populate: ['role'],
2279
+ });
2280
+
2281
+ if (user) {
2282
+ ctx.state.user = user;
2283
+ }
2284
+ }
2285
+ }
2286
+ } catch (error) {
2287
+ // JWT verification failure is expected for public endpoints - log at debug level
2288
+ strapi.log.debug(`[webbycommerce] JWT verification failed for tag route:`, error.message);
2289
+ }
2290
+ }
2291
+ }
2292
+
2293
+ // Check ecommerce permission
2294
+ const hasPermissionForTags = await ensureEcommercePermission(ctx);
2295
+ if (!hasPermissionForTags) {
2296
+ return;
2297
+ }
2298
+
2299
+ // Parse request body for POST/PUT requests if not already parsed
2300
+ const method = ctx.method.toLowerCase();
2301
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
2302
+ try {
2303
+ const contentType = ctx.request.header['content-type'] || '';
2304
+
2305
+ if (contentType.includes('application/json')) {
2306
+ const chunks = [];
2307
+ for await (const chunk of ctx.req) {
2308
+ chunks.push(chunk);
2309
+ }
2310
+ const rawBody = Buffer.concat(chunks).toString('utf8');
2311
+
2312
+ if (rawBody && rawBody.trim()) {
2313
+ ctx.request.body = JSON.parse(rawBody);
2314
+ strapi.log.debug(`[webbycommerce] Parsed request body for tags:`, ctx.request.body);
2315
+ }
2316
+ }
2317
+ } catch (error) {
2318
+ strapi.log.error(`[webbycommerce] Failed to parse request body for tag route:`, error.message);
2319
+ }
2320
+ }
2321
+
2322
+ const tagController = strapi.plugin('webbycommerce').controller('productTag');
2323
+
2324
+ if (tagController) {
2325
+ if (method === 'get' && !tagId && typeof tagController.getTags === 'function') {
2326
+ await tagController.getTags(ctx);
2327
+ return;
2328
+ }
2329
+
2330
+ if (method === 'get' && tagId && typeof tagController.getTag === 'function') {
2331
+ await tagController.getTag(ctx);
2332
+ return;
2333
+ }
2334
+
2335
+ if (method === 'post' && typeof tagController.createTag === 'function') {
2336
+ await tagController.createTag(ctx);
2337
+ return;
2338
+ }
2339
+
2340
+ if (method === 'put' && tagId && typeof tagController.updateTag === 'function') {
2341
+ await tagController.updateTag(ctx);
2342
+ return;
2343
+ }
2344
+
2345
+ if (method === 'delete' && tagId && typeof tagController.deleteTag === 'function') {
2346
+ await tagController.deleteTag(ctx);
2347
+ return;
2348
+ }
2349
+ }
2350
+ }
2351
+
2352
+ return next();
2353
+ });
2354
+
2355
+ // Handle payment routes
2356
+ strapi.server.use(async (ctx, next) => {
2357
+ // Skip admin routes - let Strapi handle them
2358
+ if (isAdminRoute(ctx.path)) {
2359
+ return next();
2360
+ }
2361
+
2362
+ const routePrefix = await getRoutePrefix();
2363
+ const originalPath = ctx.state.originalPath || ctx.path;
2364
+
2365
+ // Check both custom prefix and default prefix (after rewrite)
2366
+ const customPaymentsPath = `/api/${routePrefix}/payments`;
2367
+ const defaultPaymentsPath = `/api/webbycommerce/payments`;
2368
+ const isPaymentRoute =
2369
+ ctx.path === customPaymentsPath ||
2370
+ ctx.path.startsWith(`${customPaymentsPath}/`) ||
2371
+ ctx.path === defaultPaymentsPath ||
2372
+ ctx.path.startsWith(`${defaultPaymentsPath}/`) ||
2373
+ originalPath === customPaymentsPath ||
2374
+ originalPath.startsWith(`${customPaymentsPath}/`);
2375
+
2376
+ if (isPaymentRoute) {
2377
+ // Extract action/id from path (create-intent, confirm, webhook, :id/refund, transactions)
2378
+ let action = null;
2379
+ let paymentId = null;
2380
+ const pathMatch = ctx.path.match(/\/payments\/([^\/]+)(?:\/([^\/]+))?/);
2381
+ if (pathMatch) {
2382
+ const firstSegment = pathMatch[1];
2383
+ const secondSegment = pathMatch[2];
2384
+
2385
+ // Check if first segment is an action or an ID
2386
+ const knownActions = ['create-intent', 'confirm', 'webhook', 'transactions'];
2387
+ if (knownActions.includes(firstSegment)) {
2388
+ action = firstSegment;
2389
+ } else if (secondSegment === 'refund') {
2390
+ // Pattern: /payments/{id}/refund
2391
+ paymentId = firstSegment;
2392
+ action = secondSegment;
2393
+ } else {
2394
+ // Assume it's an ID for other operations
2395
+ paymentId = firstSegment;
2396
+ }
2397
+
2398
+ // Set ctx.params for controller access
2399
+ if (!ctx.params) {
2400
+ ctx.params = {};
2401
+ }
2402
+ if (action) {
2403
+ ctx.params.action = action;
2404
+ }
2405
+ if (paymentId) {
2406
+ ctx.params.id = paymentId;
2407
+ }
2408
+ }
2409
+
2410
+ // Authenticate user for payment routes (except webhook)
2411
+ if (action !== 'webhook') {
2412
+ const authHeader = ctx.request.header.authorization;
2413
+ if (authHeader && authHeader.startsWith('Bearer ')) {
2414
+ const token = authHeader.replace('Bearer ', '').trim();
2415
+ if (token) {
2416
+ try {
2417
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
2418
+ if (jwtService && typeof jwtService.verify === 'function') {
2419
+ const decoded = await jwtService.verify(token);
2420
+
2421
+ if (decoded && decoded.id) {
2422
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
2423
+ where: { id: decoded.id },
2424
+ populate: ['role'],
2425
+ });
2426
+
2427
+ if (user) {
2428
+ ctx.state.user = user;
2429
+ }
2430
+ }
2431
+ }
2432
+ } catch (error) {
2433
+ // JWT verification failure - log at debug level (payment may work with guest checkout)
2434
+ strapi.log.debug(`[webbycommerce] JWT verification failed for payment route:`, error.message);
2435
+ }
2436
+ }
2437
+ }
2438
+ }
2439
+
2440
+ // Check ecommerce permission for authenticated routes
2441
+ if (action !== 'webhook') {
2442
+ const hasPermission = await ensureEcommercePermission(ctx);
2443
+ if (!hasPermission) {
2444
+ return;
2445
+ }
2446
+ }
2447
+
2448
+ // Parse request body for POST/PUT requests if not already parsed
2449
+ const method = ctx.method.toLowerCase();
2450
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
2451
+ try {
2452
+ const contentType = ctx.request.header['content-type'] || '';
2453
+
2454
+ if (contentType.includes('application/json')) {
2455
+ const chunks = [];
2456
+ for await (const chunk of ctx.req) {
2457
+ chunks.push(chunk);
2458
+ }
2459
+ const rawBody = Buffer.concat(chunks).toString('utf8');
2460
+
2461
+ if (rawBody && rawBody.trim()) {
2462
+ ctx.request.body = JSON.parse(rawBody);
2463
+ strapi.log.debug(`[webbycommerce] Parsed request body for payment:`, ctx.request.body);
2464
+ }
2465
+ }
2466
+ } catch (error) {
2467
+ strapi.log.error(`[webbycommerce] Failed to parse request body for payment route:`, error.message);
2468
+ // Continue - controller will handle validation errors
2469
+ }
2470
+ }
2471
+
2472
+ const paymentController = strapi
2473
+ .plugin('webbycommerce')
2474
+ .controller('payment');
2475
+
2476
+ if (paymentController) {
2477
+ if (method === 'post' && action === 'create-intent' && typeof paymentController.createIntent === 'function') {
2478
+ await paymentController.createIntent(ctx);
2479
+ return;
2480
+ }
2481
+
2482
+ if (method === 'post' && action === 'confirm' && typeof paymentController.confirmPayment === 'function') {
2483
+ await paymentController.confirmPayment(ctx);
2484
+ return;
2485
+ }
2486
+
2487
+ if (method === 'post' && action === 'webhook' && typeof paymentController.handleWebhook === 'function') {
2488
+ await paymentController.handleWebhook(ctx);
2489
+ return;
2490
+ }
2491
+
2492
+ if (method === 'post' && action === 'refund' && typeof paymentController.processRefund === 'function') {
2493
+ await paymentController.processRefund(ctx);
2494
+ return;
2495
+ }
2496
+
2497
+ if (method === 'get' && action === 'transactions' && typeof paymentController.getTransactions === 'function') {
2498
+ await paymentController.getTransactions(ctx);
2499
+ return;
2500
+ }
2501
+ }
2502
+ }
2503
+
2504
+ return next();
2505
+ });
2506
+
2507
+ // Handle order/checkout routes
2508
+ strapi.server.use(async (ctx, next) => {
2509
+ // Skip admin routes - let Strapi handle them
2510
+ if (isAdminRoute(ctx.path)) {
2511
+ return next();
2512
+ }
2513
+
2514
+ const routePrefix = await getRoutePrefix();
2515
+ const originalPath = ctx.state.originalPath || ctx.path;
2516
+
2517
+ // Check both custom prefix and default prefix (after rewrite)
2518
+ const customOrderPath = `/api/${routePrefix}/orders`;
2519
+ const defaultOrderPath = `/api/webbycommerce/orders`;
2520
+ const customCheckoutPath = `/api/${routePrefix}/checkout`;
2521
+ const defaultCheckoutPath = `/api/webbycommerce/checkout`;
2522
+ const isOrderRoute =
2523
+ ctx.path === customOrderPath ||
2524
+ ctx.path.startsWith(`${customOrderPath}/`) ||
2525
+ ctx.path === defaultOrderPath ||
2526
+ ctx.path.startsWith(`${defaultOrderPath}/`) ||
2527
+ ctx.path === customCheckoutPath ||
2528
+ ctx.path === defaultCheckoutPath ||
2529
+ originalPath === customOrderPath ||
2530
+ originalPath.startsWith(`${customOrderPath}/`) ||
2531
+ originalPath === customCheckoutPath;
2532
+
2533
+ if (isOrderRoute) {
2534
+ // Extract ID from path if present (for specific order operations)
2535
+ let orderId = null;
2536
+ const orderPathMatch = ctx.path.match(/\/orders\/([^\/]+)/);
2537
+ if (orderPathMatch) {
2538
+ orderId = orderPathMatch[1];
2539
+ // Set ctx.params.id for controller access
2540
+ if (!ctx.params) {
2541
+ ctx.params = {};
2542
+ }
2543
+ ctx.params.id = orderId;
2544
+ }
2545
+
2546
+ // Authenticate user for order routes
2547
+ const authHeader = ctx.request.header.authorization;
2548
+ if (authHeader && authHeader.startsWith('Bearer ')) {
2549
+ const token = authHeader.replace('Bearer ', '').trim();
2550
+ if (token) {
2551
+ try {
2552
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
2553
+ if (jwtService && typeof jwtService.verify === 'function') {
2554
+ const decoded = await jwtService.verify(token);
2555
+
2556
+ if (decoded && decoded.id) {
2557
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
2558
+ where: { id: decoded.id },
2559
+ populate: ['role'],
2560
+ });
2561
+
2562
+ if (user) {
2563
+ ctx.state.user = user;
2564
+ }
2565
+ }
2566
+ }
2567
+ } catch (error) {
2568
+ // JWT verification failure - log at debug level (orders may be accessible via guest_id)
2569
+ strapi.log.debug(`[webbycommerce] JWT verification failed for order route:`, error.message);
2570
+ }
2571
+ }
2572
+ }
2573
+
2574
+ // Check ecommerce permission
2575
+ const hasPermission = await ensureEcommercePermission(ctx);
2576
+ if (!hasPermission) {
2577
+ return;
2578
+ }
2579
+
2580
+ // Parse request body for POST/PUT requests if not already parsed
2581
+ const method = ctx.method.toLowerCase();
2582
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
2583
+ try {
2584
+ const contentType = ctx.request.header['content-type'] || '';
2585
+
2586
+ if (contentType.includes('application/json')) {
2587
+ const chunks = [];
2588
+ for await (const chunk of ctx.req) {
2589
+ chunks.push(chunk);
2590
+ }
2591
+ const rawBody = Buffer.concat(chunks).toString('utf8');
2592
+
2593
+ if (rawBody && rawBody.trim()) {
2594
+ ctx.request.body = JSON.parse(rawBody);
2595
+ strapi.log.debug(`[webbycommerce] Parsed request body for order:`, ctx.request.body);
2596
+ }
2597
+ }
2598
+ } catch (error) {
2599
+ strapi.log.error(`[webbycommerce] Failed to parse request body for order route:`, error.message);
2600
+ // Continue - controller will handle validation errors
2601
+ }
2602
+ }
2603
+
2604
+ const orderController = strapi
2605
+ .plugin('webbycommerce')
2606
+ .controller('order');
2607
+
2608
+ if (orderController) {
2609
+ if (ctx.path.includes('/checkout') && method === 'post' && typeof orderController.checkout === 'function') {
2610
+ await orderController.checkout(ctx);
2611
+ return;
2612
+ }
2613
+
2614
+ if (method === 'get' && !orderId && typeof orderController.getOrders === 'function') {
2615
+ await orderController.getOrders(ctx);
2616
+ return;
2617
+ }
2618
+
2619
+ if (method === 'get' && orderId && typeof orderController.getOrder === 'function') {
2620
+ await orderController.getOrder(ctx);
2621
+ return;
2622
+ }
2623
+
2624
+ if (method === 'put' && orderId && ctx.path.includes('/cancel') && typeof orderController.cancelOrder === 'function') {
2625
+ await orderController.cancelOrder(ctx);
2626
+ return;
2627
+ }
2628
+
2629
+ if (method === 'put' && orderId && ctx.path.includes('/status') && typeof orderController.updateOrderStatus === 'function') {
2630
+ await orderController.updateOrderStatus(ctx);
2631
+ return;
2632
+ }
2633
+
2634
+ if (method === 'get' && orderId && ctx.path.includes('/tracking') && typeof orderController.getOrderTracking === 'function') {
2635
+ await orderController.getOrderTracking(ctx);
2636
+ return;
2637
+ }
2638
+ }
2639
+ }
2640
+
2641
+ return next();
2642
+ });
2643
+
2644
+ // Handle shipping routes
2645
+ strapi.server.use(async (ctx, next) => {
2646
+ // Skip admin routes - let Strapi handle them
2647
+ if (isAdminRoute(ctx.path)) {
2648
+ return next();
2649
+ }
2650
+
2651
+ const routePrefix = await getRoutePrefix();
2652
+ const originalPath = ctx.state.originalPath || ctx.path;
2653
+
2654
+ // Check both custom prefix and default prefix (after rewrite)
2655
+ const customShippingPath = `/api/${routePrefix}/shipping`;
2656
+ const defaultShippingPath = `/api/webbycommerce/shipping`;
2657
+ const isShippingRoute =
2658
+ ctx.path === customShippingPath ||
2659
+ ctx.path.startsWith(`${customShippingPath}/`) ||
2660
+ ctx.path === defaultShippingPath ||
2661
+ ctx.path.startsWith(`${defaultShippingPath}/`) ||
2662
+ originalPath === customShippingPath ||
2663
+ originalPath.startsWith(`${customShippingPath}/`);
2664
+
2665
+ if (isShippingRoute) {
2666
+ // Extract IDs from path if present
2667
+ let shippingZoneId = null;
2668
+ let shippingMethodId = null;
2669
+ let shippingRateId = null;
2670
+ let action = null;
2671
+
2672
+ // Match different shipping route patterns
2673
+ const calculateMatch = ctx.path.match(/\/shipping\/calculate$/);
2674
+ const zonesListMatch = ctx.path.match(/\/shipping\/zones$/);
2675
+ const zonesSingleMatch = ctx.path.match(/\/shipping\/zones\/([^\/]+)$/);
2676
+ const methodsListMatch = ctx.path.match(/\/shipping\/methods$/);
2677
+ const methodsSingleMatch = ctx.path.match(/\/shipping\/methods\/([^\/]+)$/);
2678
+ const ratesListMatch = ctx.path.match(/\/shipping\/methods\/([^\/]+)\/rates$/);
2679
+ const ratesSingleMatch = ctx.path.match(/\/shipping\/rates\/([^\/]+)$/);
2680
+ const ratesCreateMatch = ctx.path.match(/\/shipping\/rates$/);
2681
+
2682
+ if (calculateMatch) {
2683
+ action = 'calculate';
2684
+ } else if (zonesListMatch) {
2685
+ action = ctx.method.toLowerCase() === 'get' ? 'get-zones' : 'create-zone';
2686
+ } else if (zonesSingleMatch) {
2687
+ shippingZoneId = zonesSingleMatch[1];
2688
+ action = ctx.method.toLowerCase() === 'put' ? 'update-zone' : 'delete-zone';
2689
+ } else if (methodsListMatch) {
2690
+ action = ctx.method.toLowerCase() === 'get' ? 'get-methods' : 'create-method';
2691
+ } else if (methodsSingleMatch) {
2692
+ shippingMethodId = methodsSingleMatch[1];
2693
+ action = ctx.method.toLowerCase() === 'put' ? 'update-method' : 'delete-method';
2694
+ } else if (ratesListMatch) {
2695
+ shippingMethodId = ratesListMatch[1];
2696
+ action = 'get-rates';
2697
+ } else if (ratesCreateMatch && ctx.method.toLowerCase() === 'post') {
2698
+ action = 'create-rate';
2699
+ } else if (ratesSingleMatch) {
2700
+ shippingRateId = ratesSingleMatch[1];
2701
+ action = ctx.method.toLowerCase() === 'put' ? 'update-rate' : 'delete-rate';
2702
+ }
2703
+
2704
+ // Set ctx.params for controller access
2705
+ if (!ctx.params) {
2706
+ ctx.params = {};
2707
+ }
2708
+ if (shippingZoneId) {
2709
+ ctx.params.id = shippingZoneId;
2710
+ }
2711
+ if (shippingMethodId && action !== 'get-rates') {
2712
+ ctx.params.id = shippingMethodId;
2713
+ }
2714
+ if (shippingRateId) {
2715
+ ctx.params.id = shippingRateId;
2716
+ }
2717
+ if (shippingMethodId && action === 'get-rates') {
2718
+ ctx.params.methodId = shippingMethodId;
2719
+ }
2720
+
2721
+ // Authenticate user for shipping routes (admin routes require admin auth)
2722
+ const isAdminRoute = action && [
2723
+ 'get-zones', 'create-zone', 'update-zone', 'delete-zone',
2724
+ 'get-methods', 'create-method', 'update-method', 'delete-method',
2725
+ 'get-rates', 'create-rate', 'update-rate', 'delete-rate'
2726
+ ].includes(action);
2727
+
2728
+ if (isAdminRoute) {
2729
+ // Admin routes require admin authentication
2730
+ const authHeader = ctx.request.header.authorization;
2731
+ if (authHeader && authHeader.startsWith('Bearer ')) {
2732
+ const token = authHeader.replace('Bearer ', '').trim();
2733
+ if (token) {
2734
+ try {
2735
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
2736
+ if (jwtService && typeof jwtService.verify === 'function') {
2737
+ const decoded = await jwtService.verify(token);
2738
+
2739
+ if (decoded && decoded.id) {
2740
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
2741
+ where: { id: decoded.id },
2742
+ populate: ['role'],
2743
+ });
2744
+
2745
+ if (user) {
2746
+ ctx.state.user = user;
2747
+ // Check if user is admin
2748
+ const userRole = user.role?.type;
2749
+ if (userRole !== 'admin' && userRole !== 'super_admin') {
2750
+ ctx.forbidden('Admin access required for this operation.');
2751
+ return;
2752
+ }
2753
+ }
2754
+ }
2755
+ }
2756
+ } catch (error) {
2757
+ // JWT verification failure for admin routes - log at debug level
2758
+ strapi.log.debug(`[webbycommerce] JWT verification failed for shipping admin route:`, error.message);
2759
+ ctx.forbidden('Authentication failed.');
2760
+ return;
2761
+ }
2762
+ }
2763
+ } else {
2764
+ ctx.forbidden('Authentication required for admin operations.');
2765
+ return;
2766
+ }
2767
+ } else {
2768
+ // Frontend routes (calculate) require user authentication
2769
+ const authHeader = ctx.request.header.authorization;
2770
+ if (authHeader && authHeader.startsWith('Bearer ')) {
2771
+ const token = authHeader.replace('Bearer ', '').trim();
2772
+ if (token) {
2773
+ try {
2774
+ const jwtService = strapi.plugins['users-permissions'].services.jwt;
2775
+ if (jwtService && typeof jwtService.verify === 'function') {
2776
+ const decoded = await jwtService.verify(token);
2777
+
2778
+ if (decoded && decoded.id) {
2779
+ const user = await strapi.db.query('plugin::users-permissions.user').findOne({
2780
+ where: { id: decoded.id },
2781
+ populate: ['role'],
2782
+ });
2783
+
2784
+ if (user) {
2785
+ ctx.state.user = user;
2786
+ }
2787
+ }
2788
+ }
2789
+ } catch (error) {
2790
+ // JWT verification failure - log at debug level (shipping may be public)
2791
+ strapi.log.debug(`[webbycommerce] JWT verification failed for shipping route:`, error.message);
2792
+ }
2793
+ }
2794
+ }
2795
+ }
2796
+
2797
+ // Check ecommerce permission
2798
+ const hasPermission = await ensureEcommercePermission(ctx);
2799
+ if (!hasPermission) {
2800
+ return;
2801
+ }
2802
+
2803
+ // Parse request body for POST/PUT requests if not already parsed
2804
+ const method = ctx.method.toLowerCase();
2805
+ if ((method === 'post' || method === 'put') && (!ctx.request.body || (typeof ctx.request.body === 'object' && Object.keys(ctx.request.body || {}).length === 0))) {
2806
+ try {
2807
+ const contentType = ctx.request.header['content-type'] || '';
2808
+
2809
+ if (contentType.includes('application/json')) {
2810
+ const chunks = [];
2811
+ for await (const chunk of ctx.req) {
2812
+ chunks.push(chunk);
2813
+ }
2814
+ const rawBody = Buffer.concat(chunks).toString('utf8');
2815
+
2816
+ if (rawBody && rawBody.trim()) {
2817
+ ctx.request.body = JSON.parse(rawBody);
2818
+ strapi.log.debug(`[webbycommerce] Parsed request body for shipping:`, ctx.request.body);
2819
+ }
2820
+ }
2821
+ } catch (error) {
2822
+ strapi.log.error(`[webbycommerce] Failed to parse request body for shipping route:`, error.message);
2823
+ // Continue - controller will handle validation errors
2824
+ }
2825
+ }
2826
+
2827
+ const shippingController = strapi
2828
+ .plugin('webbycommerce')
2829
+ .controller('shipping');
2830
+
2831
+ if (shippingController) {
2832
+ if (action === 'calculate' && method === 'post' && typeof shippingController.getShippingMethods === 'function') {
2833
+ await shippingController.getShippingMethods(ctx);
2834
+ return;
2835
+ }
2836
+
2837
+ if (action === 'get-zones' && method === 'get' && typeof shippingController.getShippingZones === 'function') {
2838
+ await shippingController.getShippingZones(ctx);
2839
+ return;
2840
+ }
2841
+
2842
+ if (action === 'create-zone' && method === 'post' && typeof shippingController.createShippingZone === 'function') {
2843
+ await shippingController.createShippingZone(ctx);
2844
+ return;
2845
+ }
2846
+
2847
+ if (action === 'update-zone' && method === 'put' && typeof shippingController.updateShippingZone === 'function') {
2848
+ await shippingController.updateShippingZone(ctx);
2849
+ return;
2850
+ }
2851
+
2852
+ if (action === 'delete-zone' && method === 'delete' && typeof shippingController.deleteShippingZone === 'function') {
2853
+ await shippingController.deleteShippingZone(ctx);
2854
+ return;
2855
+ }
2856
+
2857
+ if (action === 'get-methods' && method === 'get' && typeof shippingController.getShippingMethodsAdmin === 'function') {
2858
+ await shippingController.getShippingMethodsAdmin(ctx);
2859
+ return;
2860
+ }
2861
+
2862
+ if (action === 'create-method' && method === 'post' && typeof shippingController.createShippingMethod === 'function') {
2863
+ await shippingController.createShippingMethod(ctx);
2864
+ return;
2865
+ }
2866
+
2867
+ if (action === 'update-method' && method === 'put' && typeof shippingController.updateShippingMethod === 'function') {
2868
+ await shippingController.updateShippingMethod(ctx);
2869
+ return;
2870
+ }
2871
+
2872
+ if (action === 'delete-method' && method === 'delete' && typeof shippingController.deleteShippingMethod === 'function') {
2873
+ await shippingController.deleteShippingMethod(ctx);
2874
+ return;
2875
+ }
2876
+
2877
+ if (action === 'get-rates' && method === 'get' && typeof shippingController.getShippingRates === 'function') {
2878
+ await shippingController.getShippingRates(ctx);
2879
+ return;
2880
+ }
2881
+
2882
+ if (action === 'create-rate' && method === 'post' && typeof shippingController.createShippingRate === 'function') {
2883
+ await shippingController.createShippingRate(ctx);
2884
+ return;
2885
+ }
2886
+
2887
+ if (action === 'update-rate' && method === 'put' && typeof shippingController.updateShippingRate === 'function') {
2888
+ await shippingController.updateShippingRate(ctx);
2889
+ return;
2890
+ }
2891
+
2892
+ if (action === 'delete-rate' && method === 'delete' && typeof shippingController.deleteShippingRate === 'function') {
2893
+ await shippingController.deleteShippingRate(ctx);
2894
+ return;
2895
+ }
2896
+ }
2897
+ }
2898
+
2899
+ return next();
2900
+ });
2901
+
2902
+ // Fix for content-type-builder path issue when creating/updating API content types
2903
+ // This ensures the API directory structure exists before Strapi tries to write schema files
2904
+ // IMPORTANT: This middleware must run BEFORE Strapi's content-type-builder processes the request
2905
+ strapi.server.use(async (ctx, next) => {
2906
+ // Only handle content-type-builder update-schema requests
2907
+ if (ctx.path === '/content-type-builder/update-schema' && ctx.method === 'POST') {
2908
+ try {
2909
+ // Parse body if not already parsed
2910
+ let body = ctx.request.body;
2911
+ if (!body || (typeof body === 'object' && Object.keys(body).length === 0)) {
2912
+ // Body might not be parsed yet, try to parse it manually
2913
+ try {
2914
+ const contentType = ctx.request.header['content-type'] || '';
2915
+ if (contentType.includes('application/json')) {
2916
+ const chunks = [];
2917
+ // Store the original readable stream
2918
+ const originalReq = ctx.req;
2919
+
2920
+ // Read the body
2921
+ for await (const chunk of originalReq) {
2922
+ chunks.push(chunk);
2923
+ }
2924
+ const rawBody = Buffer.concat(chunks).toString('utf8');
2925
+
2926
+ if (rawBody && rawBody.trim()) {
2927
+ body = JSON.parse(rawBody);
2928
+ ctx.request.body = body;
2929
+ // Recreate the readable stream for downstream middleware
2930
+ ctx.req = require('stream').Readable.from([Buffer.from(rawBody)]);
2931
+ }
2932
+ }
2933
+ } catch (parseError) {
2934
+ strapi.log.warn('[webbycommerce] Could not parse request body:', parseError.message);
2935
+ }
2936
+ }
2937
+
2938
+ body = body || {};
2939
+ // Handle both nested (body.data) and flat (body) request structures
2940
+ const data = body.data || body;
2941
+ const contentTypes = data.contentTypes || [];
2942
+ const components = data.components || [];
2943
+
2944
+ strapi.log.info('[webbycommerce] ===== Processing content-type-builder update-schema request =====');
2945
+ strapi.log.info('[webbycommerce] Request body keys:', Object.keys(body));
2946
+ strapi.log.info('[webbycommerce] Data keys:', Object.keys(data));
2947
+ strapi.log.info('[webbycommerce] Content types to process:', contentTypes.length);
2948
+ strapi.log.info('[webbycommerce] Components to process:', components.length);
2949
+
2950
+ if (contentTypes.length === 0 && components.length === 0) {
2951
+ strapi.log.warn('[webbycommerce] No content types or components found in request body');
2952
+ strapi.log.warn('[webbycommerce] Body type:', typeof body);
2953
+ strapi.log.warn('[webbycommerce] Body stringified (first 500 chars):', JSON.stringify(body, null, 2).substring(0, 500));
2954
+ }
2955
+
2956
+ // Get the Strapi app directory - try multiple possible locations
2957
+ let appDir;
2958
+ if (strapi.dirs && strapi.dirs.app && strapi.dirs.app.root) {
2959
+ appDir = strapi.dirs.app.root;
2960
+ strapi.log.info('[webbycommerce] Using strapi.dirs.app.root:', appDir);
2961
+ } else if (strapi.dirs && strapi.dirs.root) {
2962
+ appDir = strapi.dirs.root;
2963
+ strapi.log.info('[webbycommerce] Using strapi.dirs.root:', appDir);
2964
+ } else {
2965
+ // Fallback: __dirname is server/src, so go up two levels to get project root
2966
+ appDir = path.resolve(__dirname, '../..');
2967
+ strapi.log.info('[webbycommerce] Using fallback appDir (from __dirname):', appDir);
2968
+ strapi.log.info('[webbycommerce] __dirname is:', __dirname);
2969
+ }
2970
+
2971
+ // Ensure strapi.dirs is set for Strapi's internal use
2972
+ if (!strapi.dirs) {
2973
+ strapi.dirs = {};
2974
+ }
2975
+ if (!strapi.dirs.app) {
2976
+ strapi.dirs.app = {};
2977
+ }
2978
+ if (!strapi.dirs.app.root) {
2979
+ strapi.dirs.app.root = appDir;
2980
+ strapi.log.info('[webbycommerce] Set strapi.dirs.app.root to:', appDir);
2981
+ }
2982
+
2983
+ // Process components first (they might be referenced by content types)
2984
+ let componentsCreated = false;
2985
+ for (const component of components) {
2986
+ if (component.uid && component.uid.includes('.')) {
2987
+ const uidParts = component.uid.split('.');
2988
+ if (uidParts.length >= 2) {
2989
+ const category = uidParts[0];
2990
+ const componentName = uidParts[1];
2991
+
2992
+ strapi.log.info(`[webbycommerce] EARLY: Processing component: ${component.uid}`);
2993
+
2994
+ // Components are stored as .json files directly in src/components/{category}/
2995
+ // Format: src/components/{category}/{componentName}.json
2996
+ const componentsDir = path.join(appDir, 'src', 'components', category);
2997
+
2998
+ // Ensure category directory exists
2999
+ fs.mkdirSync(componentsDir, { recursive: true });
3000
+
3001
+ // Component file is directly in the category folder: {componentName}.json
3002
+ const componentSchemaPath = path.join(componentsDir, `${componentName}.json`);
3003
+
3004
+ // Read existing schema to preserve attributes
3005
+ let existingComponentSchema = {};
3006
+ if (fs.existsSync(componentSchemaPath)) {
3007
+ try {
3008
+ existingComponentSchema = JSON.parse(fs.readFileSync(componentSchemaPath, 'utf8'));
3009
+ } catch (error) {
3010
+ strapi.log.warn(`[webbycommerce] EARLY: Could not parse existing component schema: ${error.message}`);
3011
+ existingComponentSchema = {};
3012
+ }
3013
+ }
3014
+
3015
+ // Handle component deletion
3016
+ if (component.action === 'delete') {
3017
+ strapi.log.info(`[webbycommerce] EARLY: Deleting component: ${component.uid}`);
3018
+
3019
+ // Delete component JSON file
3020
+ if (fs.existsSync(componentSchemaPath)) {
3021
+ fs.unlinkSync(componentSchemaPath);
3022
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Deleted component file: ${componentSchemaPath}`);
3023
+ }
3024
+
3025
+ ctx.state.componentsCreated = true;
3026
+ ctx.state.componentsDeleted = true;
3027
+ continue; // Skip to next component
3028
+ }
3029
+
3030
+ // Build attributes from request
3031
+ const componentAttributes = { ...(existingComponentSchema.attributes || {}) };
3032
+
3033
+ // Process all attributes from the request
3034
+ if (component.attributes && Array.isArray(component.attributes)) {
3035
+ for (const attr of component.attributes) {
3036
+ const action = attr.action || 'update';
3037
+
3038
+ // Handle field deletion
3039
+ if (action === 'delete' && attr.name) {
3040
+ if (componentAttributes[attr.name]) {
3041
+ delete componentAttributes[attr.name];
3042
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Deleted component attribute: ${attr.name}`);
3043
+ } else {
3044
+ strapi.log.warn(`[webbycommerce] EARLY: Component attribute not found for deletion: ${attr.name}`);
3045
+ }
3046
+ continue; // Skip to next attribute
3047
+ }
3048
+
3049
+ // Handle create/update
3050
+ if (attr.name && attr.properties) {
3051
+ const attributeDef = { ...attr.properties };
3052
+ componentAttributes[attr.name] = attributeDef;
3053
+
3054
+ strapi.log.info(`[webbycommerce] EARLY: ${action === 'create' ? 'Added' : 'Updated'} component attribute: ${attr.name} (type: ${attributeDef.type || 'unknown'})`);
3055
+ }
3056
+ }
3057
+ }
3058
+
3059
+ // Build complete component schema
3060
+ // Format matches plugin components: {collectionName, info, options, attributes}
3061
+ // Category is determined by folder structure (src/components/{category}/), not in JSON
3062
+ const componentSchema = {
3063
+ collectionName: component.collectionName || existingComponentSchema.collectionName || ('components_' + component.uid.replace(/\./g, '_')),
3064
+ info: {
3065
+ displayName: component.displayName || component.modelName || existingComponentSchema.info?.displayName || componentName || 'New Component',
3066
+ description: component.description || existingComponentSchema.info?.description || '',
3067
+ icon: component.icon || existingComponentSchema.info?.icon || '',
3068
+ },
3069
+ options: component.options || existingComponentSchema.options || {},
3070
+ attributes: componentAttributes,
3071
+ };
3072
+
3073
+ // Write the complete component schema file
3074
+ const componentSchemaJson = JSON.stringify(componentSchema, null, 2);
3075
+ fs.writeFileSync(componentSchemaPath, componentSchemaJson, 'utf8');
3076
+
3077
+ // Verify the file was written correctly
3078
+ if (fs.existsSync(componentSchemaPath)) {
3079
+ try {
3080
+ const verifyComponentSchema = JSON.parse(fs.readFileSync(componentSchemaPath, 'utf8'));
3081
+ const fileStats = fs.statSync(componentSchemaPath);
3082
+
3083
+ strapi.log.info(`[webbycommerce] ========================================`);
3084
+ strapi.log.info(`[webbycommerce] ✓ COMPONENT SCHEMA CREATED/UPDATED`);
3085
+ strapi.log.info(`[webbycommerce] ========================================`);
3086
+ strapi.log.info(`[webbycommerce] ✓ Component: ${component.uid}`);
3087
+ strapi.log.info(`[webbycommerce] ✓ File: ${componentSchemaPath}`);
3088
+ strapi.log.info(`[webbycommerce] ✓ File size: ${fileStats.size} bytes`);
3089
+ strapi.log.info(`[webbycommerce] ✓ Schema is valid JSON`);
3090
+ strapi.log.info(`[webbycommerce] ✓ Display name: ${verifyComponentSchema.info?.displayName || 'N/A'}`);
3091
+ strapi.log.info(`[webbycommerce] ✓ Total attributes: ${Object.keys(verifyComponentSchema.attributes || {}).length}`);
3092
+
3093
+ // List all attributes
3094
+ const attrNames = Object.keys(verifyComponentSchema.attributes || {});
3095
+ if (attrNames.length > 0) {
3096
+ strapi.log.info(`[webbycommerce] ✓ Component attributes:`);
3097
+ attrNames.forEach(attrName => {
3098
+ const attr = verifyComponentSchema.attributes[attrName];
3099
+ const attrType = attr.type || 'unknown';
3100
+ strapi.log.info(`[webbycommerce] - ${attrName}: ${attrType}`);
3101
+ });
3102
+ }
3103
+
3104
+ strapi.log.info(`[webbycommerce] ✓ File will trigger auto-restart`);
3105
+ strapi.log.info(`[webbycommerce] ✓ After restart, component will be registered with all fields`);
3106
+ strapi.log.info(`[webbycommerce] ========================================`);
3107
+
3108
+ // Ensure file permissions and touch for file watcher
3109
+ fs.chmodSync(componentSchemaPath, 0o644);
3110
+ const now = new Date();
3111
+ fs.utimesSync(componentSchemaPath, now, now);
3112
+
3113
+ // Force file system sync to ensure the file is written to disk
3114
+ // This ensures Strapi's file watcher detects the change
3115
+ fs.fsyncSync(fs.openSync(componentSchemaPath, 'r+'));
3116
+
3117
+ componentsCreated = true;
3118
+ ctx.state.componentsCreated = true;
3119
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Set ctx.state.componentsCreated = true for component ${component.uid}`);
3120
+ strapi.log.info(`[webbycommerce] EARLY: ✓ Component file synced to disk - file watcher will detect change`);
3121
+
3122
+ } catch (verifyError) {
3123
+ strapi.log.error(`[webbycommerce] ✗ Component schema verification failed: ${verifyError.message}`);
3124
+ }
3125
+ } else {
3126
+ strapi.log.error(`[webbycommerce] ✗ Component schema file was not created: ${componentSchemaPath}`);
3127
+ }
3128
+ }
3129
+ }
3130
+ }
3131
+
3132
+ // Process each content type in the request
3133
+ for (const contentType of contentTypes) {
3134
+ if (contentType.uid && contentType.uid.startsWith('api::')) {
3135
+ // Extract API name and content type name from UID (e.g., "api::about.about")
3136
+ const uidParts = contentType.uid.split('::');
3137
+ if (uidParts.length === 2) {
3138
+ const apiAndType = uidParts[1].split('.');
3139
+ if (apiAndType.length >= 2) {
3140
+ const apiName = apiAndType[0];
3141
+ const contentTypeName = apiAndType[1];
3142
+
3143
+ const apiDir = path.join(appDir, 'src', 'api', apiName);
3144
+ const contentTypeDir = path.join(apiDir, 'content-types', contentTypeName);
3145
+ const schemaPath = path.join(contentTypeDir, 'schema.json');
3146
+
3147
+ strapi.log.info(`[webbycommerce] Processing content type: ${contentType.uid}`);
3148
+ strapi.log.info(`[webbycommerce] API Name: ${apiName}, Content Type Name: ${contentTypeName}`);
3149
+
3150
+ // Handle collection deletion
3151
+ if (contentType.action === 'delete') {
3152
+ strapi.log.info(`[webbycommerce] Deleting collection: ${contentType.uid}`);
3153
+
3154
+ // Delete schema file
3155
+ if (fs.existsSync(schemaPath)) {
3156
+ fs.unlinkSync(schemaPath);
3157
+ strapi.log.info(`[webbycommerce] ✓ Deleted schema file: ${schemaPath}`);
3158
+ }
3159
+
3160
+ // Delete content type directory (optional - Strapi will handle cleanup)
3161
+ if (fs.existsSync(contentTypeDir)) {
3162
+ try {
3163
+ fs.rmSync(contentTypeDir, { recursive: true, force: true });
3164
+ strapi.log.info(`[webbycommerce] ✓ Deleted content type directory: ${contentTypeDir}`);
3165
+ } catch (error) {
3166
+ strapi.log.warn(`[webbycommerce] Could not delete directory: ${error.message}`);
3167
+ }
3168
+ }
3169
+
3170
+ ctx.state.schemaFileCreated = true;
3171
+ ctx.state.schemaDeleted = true;
3172
+ continue; // Skip to next content type
3173
+ }
3174
+
3175
+ // ALWAYS ensure directories exist (even if they already exist, this ensures they're there)
3176
+ if (!fs.existsSync(apiDir)) {
3177
+ fs.mkdirSync(apiDir, { recursive: true });
3178
+ strapi.log.info(`[webbycommerce] ✓ Created API directory: ${apiDir}`);
3179
+ } else {
3180
+ strapi.log.info(`[webbycommerce] ✓ API directory already exists: ${apiDir}`);
3181
+ }
3182
+
3183
+ if (!fs.existsSync(contentTypeDir)) {
3184
+ fs.mkdirSync(contentTypeDir, { recursive: true });
3185
+ strapi.log.info(`[webbycommerce] ✓ Created content type directory: ${contentTypeDir}`);
3186
+ } else {
3187
+ strapi.log.info(`[webbycommerce] ✓ Content type directory already exists: ${contentTypeDir}`);
3188
+ }
3189
+
3190
+ // Ensure schema.json exists - this is critical to prevent path undefined errors
3191
+ strapi.log.info(`[webbycommerce] Schema path: ${schemaPath}`);
3192
+
3193
+ // Always ensure the schema file is written/updated to trigger Strapi's file watcher
3194
+ // This ensures auto-restart happens when new collections are added
3195
+ let schemaNeedsUpdate = false;
3196
+ let currentSchema = {};
3197
+
3198
+ if (fs.existsSync(schemaPath)) {
3199
+ try {
3200
+ currentSchema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
3201
+ if (!currentSchema || typeof currentSchema !== 'object') {
3202
+ throw new Error('Invalid schema file');
3203
+ }
3204
+ strapi.log.info(`[webbycommerce] ✓ Schema file already exists and is valid`);
3205
+ // Update the file timestamp to trigger file watcher if needed
3206
+ const now = new Date();
3207
+ fs.utimesSync(schemaPath, now, now);
3208
+ } catch (parseError) {
3209
+ strapi.log.warn(`[webbycommerce] ⚠ Schema file exists but is invalid, will be overwritten`);
3210
+ schemaNeedsUpdate = true;
3211
+ }
3212
+ } else {
3213
+ schemaNeedsUpdate = true;
3214
+ }
3215
+
3216
+ if (schemaNeedsUpdate) {
3217
+ // Build complete schema with attributes handling create/update/delete
3218
+ const attributes = { ...(currentSchema.attributes || {}) };
3219
+
3220
+ // Process all attributes from the request
3221
+ if (contentType.attributes && Array.isArray(contentType.attributes)) {
3222
+ for (const attr of contentType.attributes) {
3223
+ const action = attr.action || 'update';
3224
+
3225
+ // Handle field deletion
3226
+ if (action === 'delete' && attr.name) {
3227
+ if (attributes[attr.name]) {
3228
+ delete attributes[attr.name];
3229
+ strapi.log.info(`[webbycommerce] ✓ Deleted attribute: ${attr.name}`);
3230
+ }
3231
+ continue; // Skip to next attribute
3232
+ }
3233
+
3234
+ // Handle create/update
3235
+ if (attr.name && attr.properties) {
3236
+ attributes[attr.name] = { ...attr.properties };
3237
+ strapi.log.info(`[webbycommerce] ${action === 'create' ? 'Added' : 'Updated'} attribute: ${attr.name}`);
3238
+ }
3239
+ }
3240
+ }
3241
+
3242
+ // Create/update schema structure based on the request data
3243
+ const schema = {
3244
+ kind: contentType.kind || currentSchema.kind || 'collectionType',
3245
+ collectionName: contentType.collectionName || currentSchema.collectionName || (contentType.kind === 'singleType' ? contentTypeName : `${contentTypeName}s`),
3246
+ info: {
3247
+ singularName: contentType.singularName || currentSchema.info?.singularName || contentTypeName,
3248
+ pluralName: contentType.pluralName || currentSchema.info?.pluralName || (contentType.kind === 'singleType' ? contentTypeName : `${contentTypeName}s`),
3249
+ displayName: contentType.displayName || contentType.modelName || currentSchema.info?.displayName || contentTypeName,
3250
+ description: contentType.description || currentSchema.info?.description || '',
3251
+ },
3252
+ options: {
3253
+ draftAndPublish: contentType.draftAndPublish !== undefined ? contentType.draftAndPublish : (currentSchema.options?.draftAndPublish !== undefined ? currentSchema.options.draftAndPublish : false),
3254
+ },
3255
+ pluginOptions: contentType.pluginOptions || currentSchema.pluginOptions || {
3256
+ 'content-manager': {
3257
+ visible: true
3258
+ },
3259
+ 'content-api': {
3260
+ visible: true
3261
+ }
3262
+ },
3263
+ attributes: attributes,
3264
+ };
3265
+ fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2));
3266
+ strapi.log.info(`[webbycommerce] ✓ Created/Updated schema file: ${schemaPath}`);
3267
+ strapi.log.info(`[webbycommerce] ✓ File watcher will detect change and trigger auto-restart`);
3268
+ ctx.state.schemaFileCreated = true;
3269
+ }
3270
+
3271
+ // Also ensure the controllers, services, and routes directories exist
3272
+ const controllersDir = path.join(apiDir, 'controllers', contentTypeName);
3273
+ const servicesDir = path.join(apiDir, 'services', contentTypeName);
3274
+ const routesDir = path.join(apiDir, 'routes', contentTypeName);
3275
+
3276
+ [controllersDir, servicesDir, routesDir].forEach(dir => {
3277
+ if (!fs.existsSync(dir)) {
3278
+ fs.mkdirSync(dir, { recursive: true });
3279
+ strapi.log.info(`[webbycommerce] ✓ Created directory: ${dir}`);
3280
+ }
3281
+ });
3282
+
3283
+ // Final verification - ensure the schema path exists and is accessible
3284
+ if (!fs.existsSync(schemaPath)) {
3285
+ strapi.log.error(`[webbycommerce] ✗ CRITICAL: Schema path does not exist after creation attempt: ${schemaPath}`);
3286
+ } else {
3287
+ strapi.log.info(`[webbycommerce] ✓ Final verification: Schema path exists: ${schemaPath}`);
3288
+ }
3289
+ } else {
3290
+ strapi.log.warn(`[webbycommerce] ⚠ Could not parse UID parts for: ${contentType.uid}`);
3291
+ }
3292
+ } else {
3293
+ strapi.log.warn(`[webbycommerce] ⚠ Invalid UID format: ${contentType.uid}`);
3294
+ }
3295
+ } else {
3296
+ strapi.log.warn(`[webbycommerce] ⚠ Content type does not have UID or is not an API content type`);
3297
+ }
3298
+ }
3299
+
3300
+ strapi.log.info('[webbycommerce] ===== Finished processing content-type-builder request =====');
3301
+
3302
+ // Check if we successfully created schema files (content types or components), return success early
3303
+ // This prevents Strapi's content-type-builder from processing the request again and causing path errors
3304
+ const hasContentTypes = (ctx.state.schemaFileCreated || ctx.state.schemaDeleted) && contentTypes.length > 0;
3305
+ const hasComponents = ctx.state.componentsCreated === true || ctx.state.componentsDeleted === true;
3306
+
3307
+ strapi.log.info(`[webbycommerce] EARLY (SECOND): Checking early return conditions...`);
3308
+ strapi.log.info(`[webbycommerce] EARLY (SECOND): hasContentTypes=${hasContentTypes}, hasComponents=${hasComponents}`);
3309
+ strapi.log.info(`[webbycommerce] EARLY (SECOND): ctx.state.schemaFileCreated=${ctx.state.schemaFileCreated}, ctx.state.componentsCreated=${ctx.state.componentsCreated}`);
3310
+
3311
+ if (hasContentTypes || hasComponents) {
3312
+ strapi.log.info(`[webbycommerce] EARLY (SECOND): ✓ Schema file(s) created successfully`);
3313
+ if (hasContentTypes) {
3314
+ strapi.log.info(`[webbycommerce] EARLY (SECOND): ✓ Created ${contentTypes.length} content type(s)`);
3315
+ }
3316
+ if (hasComponents) {
3317
+ strapi.log.info(`[webbycommerce] EARLY (SECOND): ✓ Created ${components.length} component(s)`);
3318
+ }
3319
+ strapi.log.info(`[webbycommerce] EARLY (SECOND): ✓ File watcher will detect change and trigger auto-restart`);
3320
+ strapi.log.info(`[webbycommerce] EARLY (SECOND): ✓ After restart, collections and components will be automatically registered with all fields`);
3321
+
3322
+ // Return success response immediately
3323
+ // The schema files are already written, so we don't need Strapi to process them again
3324
+ // This prevents the path undefined error
3325
+ ctx.status = 200;
3326
+ // Set headers to ensure Strapi's admin panel detects the change and triggers auto-reload
3327
+ ctx.set('Content-Type', 'application/json');
3328
+ ctx.body = {
3329
+ data: {
3330
+ contentTypes: contentTypes.map(ct => {
3331
+ const uidParts = ct.uid.split('::');
3332
+ const apiAndType = uidParts.length === 2 ? uidParts[1].split('.') : [];
3333
+ return {
3334
+ uid: ct.uid,
3335
+ apiID: ct.uid,
3336
+ schema: {
3337
+ kind: ct.kind || 'collectionType',
3338
+ collectionName: ct.collectionName || (ct.kind === 'singleType' ? apiAndType[1] : `${apiAndType[1]}s`),
3339
+ info: {
3340
+ singularName: ct.singularName || apiAndType[1],
3341
+ pluralName: ct.pluralName || (ct.kind === 'singleType' ? apiAndType[1] : `${apiAndType[1]}s`),
3342
+ displayName: ct.displayName || ct.modelName || apiAndType[1],
3343
+ description: ct.description || '',
3344
+ },
3345
+ options: {
3346
+ draftAndPublish: ct.draftAndPublish !== undefined ? ct.draftAndPublish : false,
3347
+ },
3348
+ }
3349
+ };
3350
+ }),
3351
+ components: (components || []).map(comp => {
3352
+ const uidParts = comp.uid ? comp.uid.split('.') : [];
3353
+ return {
3354
+ uid: comp.uid,
3355
+ category: uidParts[0] || '',
3356
+ apiID: comp.uid,
3357
+ schema: {
3358
+ collectionName: comp.collectionName || ('components_' + comp.uid.replace(/\./g, '_')),
3359
+ info: {
3360
+ displayName: comp.displayName || comp.modelName || uidParts[1] || 'New Component',
3361
+ description: comp.description || '',
3362
+ },
3363
+ }
3364
+ };
3365
+ })
3366
+ }
3367
+ };
3368
+
3369
+ strapi.log.info(`[webbycommerce] EARLY (SECOND): ✓ Success response sent - request handled`);
3370
+ strapi.log.info(`[webbycommerce] EARLY (SECOND): ✓ Returning early to prevent Strapi from processing request again`);
3371
+ return; // Don't call next() - we've handled the request successfully
3372
+ }
3373
+ } catch (error) {
3374
+ // Log error but don't block the request - let Strapi handle it
3375
+ strapi.log.error('[webbycommerce] ✗ Error ensuring API directory structure:', error.message);
3376
+ strapi.log.error('[webbycommerce] Error stack:', error.stack);
3377
+ }
3378
+ }
3379
+
3380
+ return next();
3381
+ });
3382
+
3383
+ // Additional error handler to catch and fix path errors during content-type-builder operations
3384
+ strapi.server.use(async (ctx, next) => {
3385
+ try {
3386
+ await next();
3387
+ } catch (error) {
3388
+ // Check if this is a content-type-builder path error
3389
+ if (
3390
+ ctx.path === '/content-type-builder/update-schema' &&
3391
+ error.message &&
3392
+ error.message.includes('path') &&
3393
+ error.message.includes('undefined')
3394
+ ) {
3395
+ strapi.log.error('[webbycommerce] Caught path undefined error, attempting to fix...');
3396
+
3397
+ try {
3398
+ const body = ctx.request.body || {};
3399
+ const data = body.data || body;
3400
+ const contentTypes = data.contentTypes || [];
3401
+
3402
+ // Get the Strapi app directory
3403
+ let appDir;
3404
+ if (strapi.dirs && strapi.dirs.app && strapi.dirs.app.root) {
3405
+ appDir = strapi.dirs.app.root;
3406
+ } else if (strapi.dirs && strapi.dirs.root) {
3407
+ appDir = strapi.dirs.root;
3408
+ } else {
3409
+ appDir = path.resolve(__dirname, '../..');
3410
+ }
3411
+
3412
+ // Process each content type to ensure directories exist
3413
+ for (const contentType of contentTypes) {
3414
+ if (contentType.uid && contentType.uid.startsWith('api::')) {
3415
+ const uidParts = contentType.uid.split('::');
3416
+ if (uidParts.length === 2) {
3417
+ const apiAndType = uidParts[1].split('.');
3418
+ if (apiAndType.length >= 2) {
3419
+ const apiName = apiAndType[0];
3420
+ const contentTypeName = apiAndType[1];
3421
+
3422
+ const apiDir = path.join(appDir, 'src', 'api', apiName);
3423
+ const contentTypeDir = path.join(apiDir, 'content-types', contentTypeName);
3424
+ const schemaPath = path.join(contentTypeDir, 'schema.json');
3425
+
3426
+ // Force create directory structure
3427
+ if (!fs.existsSync(contentTypeDir)) {
3428
+ fs.mkdirSync(contentTypeDir, { recursive: true });
3429
+ strapi.log.info(`[webbycommerce] Created content type directory: ${contentTypeDir}`);
3430
+ }
3431
+
3432
+ // Ensure schema file exists
3433
+ if (!fs.existsSync(schemaPath)) {
3434
+ const minimalSchema = {
3435
+ kind: contentType.kind || 'collectionType',
3436
+ collectionName: contentType.collectionName || (contentType.kind === 'singleType' ? contentTypeName : `${contentTypeName}s`),
3437
+ info: {
3438
+ singularName: contentType.singularName || contentTypeName,
3439
+ pluralName: contentType.pluralName || (contentType.kind === 'singleType' ? contentTypeName : `${contentTypeName}s`),
3440
+ displayName: contentType.displayName || contentType.modelName || contentTypeName,
3441
+ description: contentType.description || '',
3442
+ },
3443
+ options: {
3444
+ draftAndPublish: contentType.draftAndPublish !== undefined ? contentType.draftAndPublish : false,
3445
+ },
3446
+ attributes: {},
3447
+ };
3448
+ fs.writeFileSync(schemaPath, JSON.stringify(minimalSchema, null, 2));
3449
+ strapi.log.info(`[webbycommerce] Created schema file: ${schemaPath}`);
3450
+ }
3451
+ }
3452
+ }
3453
+ }
3454
+ }
3455
+
3456
+ // Retry the request
3457
+ strapi.log.info('[webbycommerce] Retrying content-type-builder request after fixing directories...');
3458
+ // Note: We can't easily retry here, so we'll let the error propagate
3459
+ // but the directories are now created, so the next attempt should work
3460
+ } catch (fixError) {
3461
+ strapi.log.error('[webbycommerce] Failed to fix path error:', fixError.message);
3462
+ }
3463
+ }
3464
+
3465
+ // Re-throw the error so Strapi can handle it
3466
+ throw error;
3467
+ }
3468
+ });
3469
+
3470
+ // Patch content-type-builder controller to intercept and fix path errors
3471
+ // This runs after Strapi is fully loaded
3472
+ try {
3473
+ const contentTypeBuilderPlugin = strapi.plugin('content-type-builder');
3474
+ if (contentTypeBuilderPlugin) {
3475
+ const ctbController = contentTypeBuilderPlugin.controller('content-types');
3476
+ if (ctbController && typeof ctbController.updateSchema === 'function') {
3477
+ const originalUpdateSchema = ctbController.updateSchema;
3478
+ ctbController.updateSchema = async function(ctx) {
3479
+ try {
3480
+ return await originalUpdateSchema.call(this, ctx);
3481
+ } catch (error) {
3482
+ if (error.message && error.message.includes('path') && error.message.includes('undefined')) {
3483
+ strapi.log.error('[webbycommerce] CONTROLLER: Caught path undefined error in updateSchema');
3484
+
3485
+ // Get request body
3486
+ const body = ctx.request.body || {};
3487
+ const data = body.data || body;
3488
+ const contentTypes = data.contentTypes || [];
3489
+
3490
+ // Get app directory
3491
+ let appDir = strapi.dirs?.app?.root || path.resolve(__dirname, '../..');
3492
+
3493
+ // Fix all content types
3494
+ for (const contentType of contentTypes) {
3495
+ if (contentType.uid && contentType.uid.startsWith('api::')) {
3496
+ const uidParts = contentType.uid.split('::');
3497
+ if (uidParts.length === 2) {
3498
+ const apiAndType = uidParts[1].split('.');
3499
+ if (apiAndType.length >= 2) {
3500
+ const apiName = apiAndType[0];
3501
+ const contentTypeName = apiAndType[1];
3502
+ const contentTypeDir = path.join(appDir, 'src', 'api', apiName, 'content-types', contentTypeName);
3503
+ const schemaPath = path.join(contentTypeDir, 'schema.json');
3504
+
3505
+ fs.mkdirSync(contentTypeDir, { recursive: true });
3506
+ if (!fs.existsSync(schemaPath)) {
3507
+ const minimalSchema = {
3508
+ kind: contentType.kind || 'collectionType',
3509
+ collectionName: contentType.collectionName || (contentType.kind === 'singleType' ? contentTypeName : `${contentTypeName}s`),
3510
+ info: {
3511
+ singularName: contentType.singularName || contentTypeName,
3512
+ pluralName: contentType.pluralName || (contentType.kind === 'singleType' ? contentTypeName : `${contentTypeName}s`),
3513
+ displayName: contentType.displayName || contentType.modelName || contentTypeName,
3514
+ },
3515
+ options: {
3516
+ draftAndPublish: contentType.draftAndPublish !== undefined ? contentType.draftAndPublish : false,
3517
+ },
3518
+ attributes: {},
3519
+ };
3520
+ fs.writeFileSync(schemaPath, JSON.stringify(minimalSchema, null, 2));
3521
+ }
3522
+ }
3523
+ }
3524
+ }
3525
+ }
3526
+
3527
+ // Retry the original call
3528
+ strapi.log.info('[webbycommerce] CONTROLLER: Retrying updateSchema after fixing paths');
3529
+ return await originalUpdateSchema.call(this, ctx);
3530
+ }
3531
+ throw error;
3532
+ }
3533
+ };
3534
+ strapi.log.info('[webbycommerce] Patched content-type-builder updateSchema controller');
3535
+ }
3536
+
3537
+ // Also try to patch the service
3538
+ const ctbService = contentTypeBuilderPlugin.service('builder');
3539
+ if (ctbService) {
3540
+ // Patch writeContentTypeSchema if it exists
3541
+ if (ctbService.writeContentTypeSchema && typeof ctbService.writeContentTypeSchema === 'function') {
3542
+ const originalWriteContentTypeSchema = ctbService.writeContentTypeSchema;
3543
+ ctbService.writeContentTypeSchema = function(uid, schema) {
3544
+ try {
3545
+ return originalWriteContentTypeSchema.call(this, uid, schema);
3546
+ } catch (error) {
3547
+ if (error.message && error.message.includes('path') && error.message.includes('undefined')) {
3548
+ strapi.log.error('[webbycommerce] SERVICE: Caught path undefined error in writeContentTypeSchema');
3549
+
3550
+ if (uid && uid.startsWith('api::')) {
3551
+ const uidParts = uid.split('::');
3552
+ if (uidParts.length === 2) {
3553
+ const apiAndType = uidParts[1].split('.');
3554
+ if (apiAndType.length >= 2) {
3555
+ const apiName = apiAndType[0];
3556
+ const contentTypeName = apiAndType[1];
3557
+ const appDir = strapi.dirs?.app?.root || path.resolve(__dirname, '../..');
3558
+ const contentTypeDir = path.join(appDir, 'src', 'api', apiName, 'content-types', contentTypeName);
3559
+ const schemaPath = path.join(contentTypeDir, 'schema.json');
3560
+
3561
+ fs.mkdirSync(contentTypeDir, { recursive: true });
3562
+ if (!fs.existsSync(schemaPath)) {
3563
+ fs.writeFileSync(schemaPath, JSON.stringify(schema || {}, null, 2));
3564
+ }
3565
+
3566
+ // Retry
3567
+ return originalWriteContentTypeSchema.call(this, uid, schema);
3568
+ }
3569
+ }
3570
+ }
3571
+ }
3572
+ throw error;
3573
+ }
3574
+ };
3575
+ strapi.log.info('[webbycommerce] Patched content-type-builder writeContentTypeSchema service');
3576
+ }
3577
+ }
3578
+ }
3579
+ } catch (patchError) {
3580
+ strapi.log.warn('[webbycommerce] Could not patch content-type-builder:', patchError.message);
3581
+ strapi.log.warn('[webbycommerce] Patch error stack:', patchError.stack);
3582
+ }
3583
+
3584
+ // Aggressive fix: Patch fs.writeFileSync to catch undefined paths
3585
+ const originalWriteFileSync = fs.writeFileSync;
3586
+ fs.writeFileSync = function(filePath, data, options) {
3587
+ if (filePath === undefined || filePath === null) {
3588
+ const error = new Error('The "path" argument must be of type string. Received undefined');
3589
+ strapi.log.error('[webbycommerce] FS PATCH: Caught undefined path in writeFileSync');
3590
+ strapi.log.error('[webbycommerce] FS PATCH: Stack trace:', new Error().stack);
3591
+
3592
+ // Try to extract path from stack trace or context
3593
+ // This is a last resort - we should have fixed it earlier
3594
+ throw error;
3595
+ }
3596
+
3597
+ // If path is relative and doesn't exist, try to make it absolute
3598
+ if (typeof filePath === 'string' && !path.isAbsolute(filePath)) {
3599
+ const appDir = strapi.dirs?.app?.root || path.resolve(__dirname, '../..');
3600
+ const absolutePath = path.resolve(appDir, filePath);
3601
+
3602
+ // If the absolute path makes sense for a schema file, ensure directory exists
3603
+ if (absolutePath.includes('content-types') && absolutePath.endsWith('schema.json')) {
3604
+ const dir = path.dirname(absolutePath);
3605
+ if (!fs.existsSync(dir)) {
3606
+ fs.mkdirSync(dir, { recursive: true });
3607
+ strapi.log.info(`[webbycommerce] FS PATCH: Created directory for relative path: ${dir}`);
3608
+ }
3609
+ filePath = absolutePath;
3610
+ }
3611
+ }
3612
+
3613
+ return originalWriteFileSync.call(this, filePath, data, options);
3614
+ };
3615
+
3616
+ // Also patch fs.writeFile
3617
+ const originalWriteFile = fs.writeFile;
3618
+ fs.writeFile = function(filePath, data, options, callback) {
3619
+ if (filePath === undefined || filePath === null) {
3620
+ const error = new Error('The "path" argument must be of type string. Received undefined');
3621
+ strapi.log.error('[webbycommerce] FS PATCH: Caught undefined path in writeFile');
3622
+
3623
+ if (callback && typeof callback === 'function') {
3624
+ return callback(error);
3625
+ }
3626
+ throw error;
3627
+ }
3628
+
3629
+ // If path is relative and doesn't exist, try to make it absolute
3630
+ if (typeof filePath === 'string' && !path.isAbsolute(filePath)) {
3631
+ const appDir = strapi.dirs?.app?.root || path.resolve(__dirname, '../..');
3632
+ const absolutePath = path.resolve(appDir, filePath);
3633
+
3634
+ if (absolutePath.includes('content-types') && absolutePath.endsWith('schema.json')) {
3635
+ const dir = path.dirname(absolutePath);
3636
+ if (!fs.existsSync(dir)) {
3637
+ fs.mkdirSync(dir, { recursive: true });
3638
+ strapi.log.info(`[webbycommerce] FS PATCH: Created directory for relative path: ${dir}`);
3639
+ }
3640
+ filePath = absolutePath;
3641
+ }
3642
+ }
3643
+
3644
+ return originalWriteFile.call(this, filePath, data, options, callback);
3645
+ };
3646
+
3647
+ strapi.log.info('[webbycommerce] Patched fs.writeFileSync and fs.writeFile to catch undefined paths');
3648
+
3649
+ // Register ecommerce actions with retry logic
3650
+ let retryCount = 0;
3651
+ const maxRetries = 3;
3652
+ const retryDelay = 1000; // 1 second
3653
+
3654
+ const registerWithRetry = async () => {
3655
+ try {
3656
+ await registerEcommerceActions();
3657
+ strapi.log.info('[webbycommerce] Ecommerce actions registered successfully');
3658
+
3659
+ return true;
3660
+ } catch (error) {
3661
+ strapi.log.warn(`[webbycommerce] Failed to register ecommerce actions (attempt ${retryCount + 1}/${maxRetries}):`, error.message);
3662
+
3663
+ if (retryCount < maxRetries - 1) {
3664
+ retryCount++;
3665
+ strapi.log.info(`[webbycommerce] Retrying in ${retryDelay}ms...`);
3666
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
3667
+ return registerWithRetry();
3668
+ } else {
3669
+ strapi.log.error('[webbycommerce] Failed to register ecommerce actions after all retries');
3670
+ throw error;
3671
+ }
3672
+ }
3673
+ };
3674
+
3675
+ await registerWithRetry();
3676
+
3677
+ // Hook to detect new content types and ensure they're registered
3678
+ // This helps with auto-restart and collection registration
3679
+ strapi.db.lifecycles.subscribe({
3680
+ models: ['*'], // Listen to all models
3681
+ async afterCreate(event) {
3682
+ // This runs after any content type entry is created
3683
+ // We can use this to detect new collections
3684
+ },
3685
+ });
3686
+
3687
+ // Monitor content-type-builder for successful schema updates
3688
+ // This ensures new collections trigger auto-restart
3689
+ strapi.server.use(async (ctx, next) => {
3690
+ if (ctx.path === '/content-type-builder/update-schema' && ctx.method === 'POST') {
3691
+ await next();
3692
+
3693
+ // After the request completes, check if it was successful
3694
+ if (ctx.status === 200 || ctx.status === 201) {
3695
+ const body = ctx.request.body || {};
3696
+ const data = body.data || body;
3697
+ const contentTypes = data.contentTypes || [];
3698
+
3699
+ for (const contentType of contentTypes) {
3700
+ if (contentType.uid && contentType.uid.startsWith('api::')) {
3701
+ const uidParts = contentType.uid.split('::');
3702
+ if (uidParts.length === 2) {
3703
+ const apiAndType = uidParts[1].split('.');
3704
+ if (apiAndType.length >= 2) {
3705
+ const apiName = apiAndType[0];
3706
+ const contentTypeName = apiAndType[1];
3707
+
3708
+ strapi.log.info(`[webbycommerce] ✓ New collection created: ${contentType.uid}`);
3709
+ strapi.log.info(`[webbycommerce] ✓ Collection will be auto-registered on next restart`);
3710
+ strapi.log.info(`[webbycommerce] ✓ Strapi will auto-restart in develop mode to register the new collection`);
3711
+
3712
+ // In develop mode, Strapi automatically restarts when schema files change
3713
+ // This is handled by Strapi's file watcher, so we just need to ensure the file exists
3714
+ const appDir = strapi.dirs?.app?.root || path.resolve(__dirname, '../..');
3715
+ const schemaPath = path.join(appDir, 'src', 'api', apiName, 'content-types', contentTypeName, 'schema.json');
3716
+
3717
+ if (fs.existsSync(schemaPath)) {
3718
+ strapi.log.info(`[webbycommerce] ✓ Schema file confirmed: ${schemaPath}`);
3719
+ strapi.log.info(`[webbycommerce] ✓ Auto-restart should occur automatically in develop mode`);
3720
+ }
3721
+ }
3722
+ }
3723
+ }
3724
+ }
3725
+ }
3726
+ } else {
3727
+ await next();
3728
+ }
3729
+ });
3730
+
3731
+ // Log all registered content types and components on startup
3732
+ try {
3733
+ const allContentTypes = strapi.contentTypes;
3734
+ const apiContentTypes = Object.keys(allContentTypes).filter(uid => uid.startsWith('api::'));
3735
+ strapi.log.info(`[webbycommerce] Currently registered API content types: ${apiContentTypes.length}`);
3736
+ if (apiContentTypes.length > 0) {
3737
+ strapi.log.info(`[webbycommerce] Registered collections: ${apiContentTypes.join(', ')}`);
3738
+ }
3739
+
3740
+ // Log registered components - check multiple ways Strapi stores components
3741
+ try {
3742
+ // Try different ways to access components
3743
+ let componentKeys = [];
3744
+
3745
+ // Method 1: strapi.components (Map)
3746
+ if (strapi.components && strapi.components instanceof Map) {
3747
+ componentKeys = Array.from(strapi.components.keys());
3748
+ }
3749
+ // Method 2: strapi.get('components')
3750
+ else if (strapi.get && typeof strapi.get === 'function') {
3751
+ const components = strapi.get('components');
3752
+ if (components instanceof Map) {
3753
+ componentKeys = Array.from(components.keys());
3754
+ } else if (components && typeof components === 'object') {
3755
+ componentKeys = Object.keys(components);
3756
+ }
3757
+ }
3758
+ // Method 3: strapi.components as object
3759
+ else if (strapi.components && typeof strapi.components === 'object') {
3760
+ componentKeys = Object.keys(strapi.components);
3761
+ }
3762
+
3763
+ // Filter for user-created components (not plugin components)
3764
+ const userComponents = componentKeys.filter(uid =>
3765
+ (uid.startsWith('shared.') || uid.includes('.')) &&
3766
+ !uid.startsWith('plugin::')
3767
+ );
3768
+
3769
+ strapi.log.info(`[webbycommerce] Currently registered user components: ${userComponents.length}`);
3770
+ if (userComponents.length > 0) {
3771
+ strapi.log.info(`[webbycommerce] Registered components: ${userComponents.join(', ')}`);
3772
+ } else {
3773
+ strapi.log.warn(`[webbycommerce] ⚠ No user components found - checking component files...`);
3774
+
3775
+ // Check if component files exist
3776
+ // Components are stored as .json files directly in category folders: src/components/{category}/{componentName}.json
3777
+ const appDir = strapi.dirs?.app?.root || path.resolve(__dirname, '../..');
3778
+ const componentsDir = path.join(appDir, 'src', 'components');
3779
+ if (fs.existsSync(componentsDir)) {
3780
+ const categoryDirs = fs.readdirSync(componentsDir, { withFileTypes: true })
3781
+ .filter(dirent => dirent.isDirectory())
3782
+ .map(dirent => dirent.name);
3783
+
3784
+ let totalComponentFiles = 0;
3785
+ for (const category of categoryDirs) {
3786
+ const categoryPath = path.join(componentsDir, category);
3787
+ // Look for .json files directly in the category folder
3788
+ const files = fs.readdirSync(categoryPath, { withFileTypes: true })
3789
+ .filter(dirent => dirent.isFile() && dirent.name.endsWith('.json'))
3790
+ .map(dirent => dirent.name);
3791
+
3792
+ for (const jsonFile of files) {
3793
+ const componentName = jsonFile.replace('.json', '');
3794
+ const componentPath = path.join(categoryPath, jsonFile);
3795
+ if (fs.existsSync(componentPath)) {
3796
+ totalComponentFiles++;
3797
+ strapi.log.info(`[webbycommerce] - Found component file: ${category}.${componentName} at ${componentPath}`);
3798
+ }
3799
+ }
3800
+ }
3801
+
3802
+ if (totalComponentFiles > 0) {
3803
+ strapi.log.warn(`[webbycommerce] ⚠ Found ${totalComponentFiles} component files but Strapi hasn't loaded them yet`);
3804
+ strapi.log.warn(`[webbycommerce] ⚠ Components should appear after Strapi finishes loading - try refreshing the browser`);
3805
+ }
3806
+ }
3807
+ }
3808
+ } catch (compError) {
3809
+ strapi.log.debug(`[webbycommerce] Could not list components: ${compError.message}`);
3810
+ }
3811
+ } catch (error) {
3812
+ // Ignore errors in logging
3813
+ }
3814
+
3815
+ strapi.log.info('[webbycommerce] Plugin bootstrapped successfully');
3816
+ strapi.log.info(
3817
+ '[webbycommerce] Health endpoint is available at: /webbycommerce/health and /api/webbycommerce/health'
3818
+ );
3819
+ strapi.log.info('[webbycommerce] Auto-restart enabled: Strapi will automatically restart when new collections or components are added');
3820
+ strapi.log.info('[webbycommerce] ========================================');
3821
+ } catch (error) {
3822
+ strapi.log.error('[webbycommerce] Bootstrap error:', error);
3823
+ throw error;
3824
+ }
3825
+ };
3826
+