@webbycrown/webbycommerce 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -3
- package/admin/app.js +3 -0
- package/admin/jsconfig.json +20 -0
- package/admin/src/components/ApiCollectionsContent.jsx +4626 -0
- package/admin/src/components/CompareContent.jsx +300 -0
- package/admin/src/components/ConfigureContent.jsx +407 -0
- package/admin/src/components/Initializer.jsx +64 -0
- package/admin/src/components/LoginRegisterContent.jsx +280 -0
- package/admin/src/components/PluginIcon.jsx +6 -0
- package/admin/src/components/ShippingTypeContent.jsx +230 -0
- package/admin/src/components/SmtpContent.jsx +316 -0
- package/admin/src/components/WishlistContent.jsx +273 -0
- package/admin/src/index.js +81 -0
- package/admin/src/pages/ApiCollections.jsx +169 -0
- package/admin/src/pages/Configure.jsx +55 -0
- package/admin/src/pages/Settings.jsx +93 -0
- package/admin/src/pluginId.js +4 -0
- package/{dist/_chunks/en-CiQ97iC8.js → admin/src/translations/en.json} +712 -574
- package/bin/setup.js +50 -3
- package/package.json +14 -13
- package/server/bootstrap.js +3 -0
- package/server/register.js +3 -0
- package/server/src/bootstrap.js +3826 -0
- package/server/src/components/content-block.json +37 -0
- package/server/src/components/shipping-zone-location.json +27 -0
- package/server/src/config/index.js +7 -0
- package/server/src/content-types/address/index.js +7 -0
- package/server/src/content-types/address/schema.json +74 -0
- package/server/src/content-types/cart/index.js +61 -0
- package/server/src/content-types/cart-item/index.js +79 -0
- package/server/src/content-types/compare.js +73 -0
- package/server/src/content-types/coupon/index.js +7 -0
- package/server/src/content-types/coupon/schema.json +67 -0
- package/server/src/content-types/index.js +42 -0
- package/server/src/content-types/order/index.js +7 -0
- package/server/src/content-types/order/schema.json +121 -0
- package/server/src/content-types/payment-transaction/index.js +7 -0
- package/server/src/content-types/payment-transaction/schema.json +73 -0
- package/server/src/content-types/product/index.js +7 -0
- package/server/src/content-types/product/schema.json +104 -0
- package/server/src/content-types/product-attribute/index.js +7 -0
- package/server/src/content-types/product-attribute/schema.json +80 -0
- package/server/src/content-types/product-attribute-value/index.js +7 -0
- package/server/src/content-types/product-attribute-value/schema.json +52 -0
- package/server/src/content-types/product-category/index.js +7 -0
- package/server/src/content-types/product-category/schema.json +54 -0
- package/server/src/content-types/product-tag/index.js +7 -0
- package/server/src/content-types/product-tag/schema.json +38 -0
- package/server/src/content-types/product-variation/index.js +7 -0
- package/server/src/content-types/product-variation/schema.json +74 -0
- package/server/src/content-types/shipping-method/index.js +7 -0
- package/server/src/content-types/shipping-method/schema.json +91 -0
- package/server/src/content-types/shipping-rate/index.js +7 -0
- package/server/src/content-types/shipping-rate/schema.json +73 -0
- package/server/src/content-types/shipping-rule/index.js +7 -0
- package/server/src/content-types/shipping-rule/schema.json +84 -0
- package/server/src/content-types/shipping-zone/index.js +7 -0
- package/server/src/content-types/shipping-zone/schema.json +57 -0
- package/server/src/content-types/wishlist.js +66 -0
- package/server/src/controllers/address.js +374 -0
- package/server/src/controllers/auth.js +1409 -0
- package/server/src/controllers/cart.js +337 -0
- package/server/src/controllers/category.js +388 -0
- package/server/src/controllers/compare.js +246 -0
- package/server/src/controllers/controller.js +168 -0
- package/server/src/controllers/ecommerce.js +20 -0
- package/server/src/controllers/index.js +34 -0
- package/server/src/controllers/order.js +1100 -0
- package/server/src/controllers/payment.js +243 -0
- package/server/src/controllers/product.js +1006 -0
- package/server/src/controllers/productTag.js +370 -0
- package/server/src/controllers/productVariation.js +181 -0
- package/server/src/controllers/shipping.js +1046 -0
- package/server/src/controllers/wishlist.js +332 -0
- package/server/src/destroy.js +6 -0
- package/server/src/index.js +26 -0
- package/server/src/middlewares/index.js +4 -0
- package/server/src/policies/index.js +4 -0
- package/server/src/register.js +67 -0
- package/server/src/routes/index.js +1130 -0
- package/server/src/services/cart.js +531 -0
- package/server/src/services/compare.js +300 -0
- package/server/src/services/index.js +16 -0
- package/server/src/services/service.js +19 -0
- package/server/src/services/shipping.js +513 -0
- package/server/src/services/wishlist.js +238 -0
- package/server/src/utils/check-ecommerce-permission.js +204 -0
- package/server/src/utils/extend-user-schema.js +161 -0
- package/server/src/utils/seed-data.js +639 -0
- package/server/src/utils/send-email.js +98 -0
- package/strapi-server.js +1 -6
- package/dist/_chunks/Settings-DZXAkI24.js +0 -31539
- package/dist/_chunks/Settings-yLx-YvVy.mjs +0 -31520
- package/dist/_chunks/en-DE15m4xZ.mjs +0 -574
- package/dist/_chunks/index-CXGrFKp6.mjs +0 -128
- package/dist/_chunks/index-DgocXUgC.js +0 -127
- package/dist/admin/index.js +0 -3
- package/dist/admin/index.mjs +0 -4
- package/dist/robots.txt +0 -3
- package/dist/server/index.js +0 -27078
- package/dist/uploads/.gitkeep +0 -0
- package/dist/uploads/accessories_category_2a5631094b.jpeg +0 -0
- package/dist/uploads/beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
- package/dist/uploads/books_category_a9a253eada.jpeg +0 -0
- package/dist/uploads/classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/clothing_category_d5c60ef07b.jpeg +0 -0
- package/dist/uploads/daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/electronics_category_fc3e5ef571.jpeg +0 -0
- package/dist/uploads/ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/home_garden_category_4f6eb3f8d6.jpeg +0 -0
- package/dist/uploads/istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
- package/dist/uploads/istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
- package/dist/uploads/large_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/medium_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/medium_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/medium_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/medium_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/medium_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/medium_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/medium_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/medium_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/medium_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/medium_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
- package/dist/uploads/small_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/small_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/small_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/small_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/small_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/small_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/small_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/small_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/small_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/small_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/thumbnail_accessories_category_2a5631094b.jpeg +0 -0
- package/dist/uploads/thumbnail_beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
- package/dist/uploads/thumbnail_books_category_a9a253eada.jpeg +0 -0
- package/dist/uploads/thumbnail_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/thumbnail_clothing_category_d5c60ef07b.jpeg +0 -0
- package/dist/uploads/thumbnail_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/thumbnail_electronics_category_fc3e5ef571.jpeg +0 -0
- package/dist/uploads/thumbnail_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/thumbnail_home_garden_category_4f6eb3f8d6.jpeg +0 -0
- package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
- package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
- package/dist/uploads/thumbnail_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/thumbnail_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/thumbnail_predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
- package/dist/uploads/thumbnail_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/thumbnail_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/thumbnail_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/thumbnail_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/thumbnail_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/webby-commerce.png +0 -0
- package/dist/uploads/wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- /package/{dist → server/src}/data/demo-data.json +0 -0
|
@@ -0,0 +1,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
|
+
|