@webbycrown/webbycommerce 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +21 -3
  2. package/admin/app.js +3 -0
  3. package/admin/jsconfig.json +20 -0
  4. package/admin/src/components/ApiCollectionsContent.jsx +4626 -0
  5. package/admin/src/components/CompareContent.jsx +300 -0
  6. package/admin/src/components/ConfigureContent.jsx +407 -0
  7. package/admin/src/components/Initializer.jsx +64 -0
  8. package/admin/src/components/LoginRegisterContent.jsx +280 -0
  9. package/admin/src/components/PluginIcon.jsx +6 -0
  10. package/admin/src/components/ShippingTypeContent.jsx +230 -0
  11. package/admin/src/components/SmtpContent.jsx +316 -0
  12. package/admin/src/components/WishlistContent.jsx +273 -0
  13. package/admin/src/index.js +81 -0
  14. package/admin/src/pages/ApiCollections.jsx +169 -0
  15. package/admin/src/pages/Configure.jsx +55 -0
  16. package/admin/src/pages/Settings.jsx +93 -0
  17. package/admin/src/pluginId.js +4 -0
  18. package/{dist/_chunks/en-CiQ97iC8.js → admin/src/translations/en.json} +712 -574
  19. package/bin/setup.js +50 -3
  20. package/package.json +14 -13
  21. package/server/bootstrap.js +3 -0
  22. package/server/register.js +3 -0
  23. package/server/src/bootstrap.js +3826 -0
  24. package/server/src/components/content-block.json +37 -0
  25. package/server/src/components/shipping-zone-location.json +27 -0
  26. package/server/src/config/index.js +7 -0
  27. package/server/src/content-types/address/index.js +7 -0
  28. package/server/src/content-types/address/schema.json +74 -0
  29. package/server/src/content-types/cart/index.js +61 -0
  30. package/server/src/content-types/cart-item/index.js +79 -0
  31. package/server/src/content-types/compare.js +73 -0
  32. package/server/src/content-types/coupon/index.js +7 -0
  33. package/server/src/content-types/coupon/schema.json +67 -0
  34. package/server/src/content-types/index.js +42 -0
  35. package/server/src/content-types/order/index.js +7 -0
  36. package/server/src/content-types/order/schema.json +121 -0
  37. package/server/src/content-types/payment-transaction/index.js +7 -0
  38. package/server/src/content-types/payment-transaction/schema.json +73 -0
  39. package/server/src/content-types/product/index.js +7 -0
  40. package/server/src/content-types/product/schema.json +104 -0
  41. package/server/src/content-types/product-attribute/index.js +7 -0
  42. package/server/src/content-types/product-attribute/schema.json +80 -0
  43. package/server/src/content-types/product-attribute-value/index.js +7 -0
  44. package/server/src/content-types/product-attribute-value/schema.json +52 -0
  45. package/server/src/content-types/product-category/index.js +7 -0
  46. package/server/src/content-types/product-category/schema.json +54 -0
  47. package/server/src/content-types/product-tag/index.js +7 -0
  48. package/server/src/content-types/product-tag/schema.json +38 -0
  49. package/server/src/content-types/product-variation/index.js +7 -0
  50. package/server/src/content-types/product-variation/schema.json +74 -0
  51. package/server/src/content-types/shipping-method/index.js +7 -0
  52. package/server/src/content-types/shipping-method/schema.json +91 -0
  53. package/server/src/content-types/shipping-rate/index.js +7 -0
  54. package/server/src/content-types/shipping-rate/schema.json +73 -0
  55. package/server/src/content-types/shipping-rule/index.js +7 -0
  56. package/server/src/content-types/shipping-rule/schema.json +84 -0
  57. package/server/src/content-types/shipping-zone/index.js +7 -0
  58. package/server/src/content-types/shipping-zone/schema.json +57 -0
  59. package/server/src/content-types/wishlist.js +66 -0
  60. package/server/src/controllers/address.js +374 -0
  61. package/server/src/controllers/auth.js +1409 -0
  62. package/server/src/controllers/cart.js +337 -0
  63. package/server/src/controllers/category.js +388 -0
  64. package/server/src/controllers/compare.js +246 -0
  65. package/server/src/controllers/controller.js +168 -0
  66. package/server/src/controllers/ecommerce.js +20 -0
  67. package/server/src/controllers/index.js +34 -0
  68. package/server/src/controllers/order.js +1100 -0
  69. package/server/src/controllers/payment.js +243 -0
  70. package/server/src/controllers/product.js +1006 -0
  71. package/server/src/controllers/productTag.js +370 -0
  72. package/server/src/controllers/productVariation.js +181 -0
  73. package/server/src/controllers/shipping.js +1046 -0
  74. package/server/src/controllers/wishlist.js +332 -0
  75. package/server/src/destroy.js +6 -0
  76. package/server/src/index.js +26 -0
  77. package/server/src/middlewares/index.js +4 -0
  78. package/server/src/policies/index.js +4 -0
  79. package/server/src/register.js +67 -0
  80. package/server/src/routes/index.js +1130 -0
  81. package/server/src/services/cart.js +531 -0
  82. package/server/src/services/compare.js +300 -0
  83. package/server/src/services/index.js +16 -0
  84. package/server/src/services/service.js +19 -0
  85. package/server/src/services/shipping.js +513 -0
  86. package/server/src/services/wishlist.js +238 -0
  87. package/server/src/utils/check-ecommerce-permission.js +204 -0
  88. package/server/src/utils/extend-user-schema.js +161 -0
  89. package/server/src/utils/seed-data.js +639 -0
  90. package/server/src/utils/send-email.js +98 -0
  91. package/strapi-server.js +1 -6
  92. package/dist/_chunks/Settings-Bg2JyQ4c.js +0 -31518
  93. package/dist/_chunks/Settings-BonPzbwr.mjs +0 -31499
  94. package/dist/_chunks/en-DE15m4xZ.mjs +0 -574
  95. package/dist/_chunks/index-BWVy9o1d.mjs +0 -128
  96. package/dist/_chunks/index-NRuOdjd7.js +0 -127
  97. package/dist/admin/index.js +0 -3
  98. package/dist/admin/index.mjs +0 -4
  99. package/dist/robots.txt +0 -3
  100. package/dist/server/index.js +0 -27336
  101. package/dist/uploads/.gitkeep +0 -0
  102. package/dist/uploads/accessories_category_2a5631094b.jpeg +0 -0
  103. package/dist/uploads/beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
  104. package/dist/uploads/books_category_a9a253eada.jpeg +0 -0
  105. package/dist/uploads/classic_cotton_tshirt_1_cd713425f6.png +0 -0
  106. package/dist/uploads/clothing_category_d5c60ef07b.jpeg +0 -0
  107. package/dist/uploads/daviddoe_strapi_adbcd41787.jpeg +0 -0
  108. package/dist/uploads/electronics_category_fc3e5ef571.jpeg +0 -0
  109. package/dist/uploads/ergonomic_office_chair_1_c751cffb07.png +0 -0
  110. package/dist/uploads/home_garden_category_4f6eb3f8d6.jpeg +0 -0
  111. package/dist/uploads/istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
  112. package/dist/uploads/istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
  113. package/dist/uploads/large_daviddoe_strapi_adbcd41787.jpeg +0 -0
  114. package/dist/uploads/leather_travel_backpack_1_238bc1ae4d.png +0 -0
  115. package/dist/uploads/mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  116. package/dist/uploads/medium_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  117. package/dist/uploads/medium_daviddoe_strapi_adbcd41787.jpeg +0 -0
  118. package/dist/uploads/medium_ergonomic_office_chair_1_c751cffb07.png +0 -0
  119. package/dist/uploads/medium_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  120. package/dist/uploads/medium_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  121. package/dist/uploads/medium_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  122. package/dist/uploads/medium_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  123. package/dist/uploads/medium_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  124. package/dist/uploads/medium_wireless_headphones_1_fa75cd50c3.png +0 -0
  125. package/dist/uploads/medium_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  126. package/dist/uploads/predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
  127. package/dist/uploads/small_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  128. package/dist/uploads/small_daviddoe_strapi_adbcd41787.jpeg +0 -0
  129. package/dist/uploads/small_ergonomic_office_chair_1_c751cffb07.png +0 -0
  130. package/dist/uploads/small_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  131. package/dist/uploads/small_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  132. package/dist/uploads/small_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  133. package/dist/uploads/small_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  134. package/dist/uploads/small_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  135. package/dist/uploads/small_wireless_headphones_1_fa75cd50c3.png +0 -0
  136. package/dist/uploads/small_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  137. package/dist/uploads/smart_watch_series_5_1_cdc2511fb7.png +0 -0
  138. package/dist/uploads/smartphone_x_pro_1_c3f0cbd080.png +0 -0
  139. package/dist/uploads/the_great_gatsby_special_1_2e7c76d997.png +0 -0
  140. package/dist/uploads/thumbnail_accessories_category_2a5631094b.jpeg +0 -0
  141. package/dist/uploads/thumbnail_beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
  142. package/dist/uploads/thumbnail_books_category_a9a253eada.jpeg +0 -0
  143. package/dist/uploads/thumbnail_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  144. package/dist/uploads/thumbnail_clothing_category_d5c60ef07b.jpeg +0 -0
  145. package/dist/uploads/thumbnail_daviddoe_strapi_adbcd41787.jpeg +0 -0
  146. package/dist/uploads/thumbnail_electronics_category_fc3e5ef571.jpeg +0 -0
  147. package/dist/uploads/thumbnail_ergonomic_office_chair_1_c751cffb07.png +0 -0
  148. package/dist/uploads/thumbnail_home_garden_category_4f6eb3f8d6.jpeg +0 -0
  149. package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
  150. package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
  151. package/dist/uploads/thumbnail_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  152. package/dist/uploads/thumbnail_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  153. package/dist/uploads/thumbnail_predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
  154. package/dist/uploads/thumbnail_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  155. package/dist/uploads/thumbnail_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  156. package/dist/uploads/thumbnail_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  157. package/dist/uploads/thumbnail_wireless_headphones_1_fa75cd50c3.png +0 -0
  158. package/dist/uploads/thumbnail_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  159. package/dist/uploads/webby-commerce.png +0 -0
  160. package/dist/uploads/wireless_headphones_1_fa75cd50c3.png +0 -0
  161. package/dist/uploads/yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  162. /package/{dist → server/src}/data/demo-data.json +0 -0
@@ -0,0 +1,238 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * wishlist service
5
+ */
6
+
7
+ const { createCoreService } = require('@strapi/strapi').factories;
8
+
9
+ module.exports = createCoreService('plugin::webbycommerce.wishlist', ({ strapi }) => ({
10
+ async findUserWishlist(userId) {
11
+ try {
12
+ const wishlists = await strapi.db.query('plugin::webbycommerce.wishlist').findMany({
13
+ where: {
14
+ userId: String(userId),
15
+ },
16
+ orderBy: { createdAt: 'desc' },
17
+ populate: {
18
+ products: {
19
+ populate: {
20
+ images: true,
21
+ product_categories: true,
22
+ tags: true,
23
+ variations: {
24
+ populate: {
25
+ attributes: true,
26
+ attributeValues: true,
27
+ },
28
+ },
29
+ },
30
+ },
31
+ },
32
+ });
33
+
34
+ return wishlists.length > 0 ? wishlists[0] : null;
35
+ } catch (error) {
36
+ throw new Error(`Failed to find user wishlist: ${error.message}`);
37
+ }
38
+ },
39
+
40
+ async createUserWishlist(userId, userEmail, data = {}) {
41
+ try {
42
+ const wishlistData = {
43
+ userId: String(userId),
44
+ userEmail,
45
+ products: [],
46
+ isPublic: data.isPublic || false,
47
+ name: data.name || null,
48
+ description: data.description || null,
49
+ };
50
+
51
+ await strapi.entityService.create('plugin::webbycommerce.wishlist', {
52
+ data: wishlistData,
53
+ });
54
+
55
+ // Refetch wishlist with populated products
56
+ return await this.findUserWishlist(userId);
57
+ } catch (error) {
58
+ throw new Error(`Failed to create user wishlist: ${error.message}`);
59
+ }
60
+ },
61
+
62
+ async addProductToWishlist(userId, userEmail, productId) {
63
+ try {
64
+ // Find existing wishlist or create new one
65
+ let wishlist = await this.findUserWishlist(userId);
66
+
67
+ if (!wishlist) {
68
+ wishlist = await this.createUserWishlist(userId, userEmail);
69
+ }
70
+
71
+ // Ensure products array exists (for newly created wishlists)
72
+ if (!wishlist.products) {
73
+ wishlist.products = [];
74
+ }
75
+
76
+ // Check if product already exists in wishlist
77
+ // Handle both populated (object with id) and unpopulated (just id) cases
78
+ const productExists = wishlist.products.some(product => {
79
+ const productIdValue = typeof product === 'object' && product !== null ? product.id : product;
80
+ return productIdValue === parseInt(productId);
81
+ });
82
+
83
+ if (productExists) {
84
+ throw new Error('Product already exists in wishlist');
85
+ }
86
+
87
+ // Verify product exists (check both published and draft products)
88
+ const product = await strapi.db.query('plugin::webbycommerce.product').findOne({
89
+ where: { id: productId },
90
+ populate: ['product_categories', 'tags', 'images', 'variations'],
91
+ });
92
+ if (!product) {
93
+ throw new Error('Product not found');
94
+ }
95
+
96
+ // Get existing product IDs (handle both populated and unpopulated cases)
97
+ const existingProductIds = wishlist.products.map(p => {
98
+ return typeof p === 'object' && p !== null ? p.id : p;
99
+ }).filter(id => id != null);
100
+
101
+ // Add product to wishlist
102
+ await strapi.entityService.update('plugin::webbycommerce.wishlist', wishlist.id, {
103
+ data: {
104
+ products: { set: [...existingProductIds, parseInt(productId, 10)].map((id) => ({ id })) },
105
+ },
106
+ });
107
+
108
+ // Refetch wishlist with populated products
109
+ return await this.findUserWishlist(userId);
110
+ } catch (error) {
111
+ throw new Error(`Failed to add product to wishlist: ${error.message}`);
112
+ }
113
+ },
114
+
115
+ async removeProductFromWishlist(userId, productId) {
116
+ try {
117
+ const wishlist = await this.findUserWishlist(userId);
118
+
119
+ if (!wishlist) {
120
+ throw new Error('Wishlist not found');
121
+ }
122
+
123
+ // Ensure products array exists
124
+ if (!wishlist.products) {
125
+ wishlist.products = [];
126
+ }
127
+
128
+ // Remove product from wishlist
129
+ // Handle both populated (object with id) and unpopulated (just id) cases
130
+ const updatedProducts = wishlist.products
131
+ .filter(product => {
132
+ const productIdValue = typeof product === 'object' && product !== null ? product.id : product;
133
+ return productIdValue !== parseInt(productId);
134
+ })
135
+ .map(product => {
136
+ return typeof product === 'object' && product !== null ? product.id : product;
137
+ });
138
+
139
+ await strapi.entityService.update('plugin::webbycommerce.wishlist', wishlist.id, {
140
+ data: {
141
+ products: { set: updatedProducts.map((id) => ({ id })) },
142
+ },
143
+ });
144
+
145
+ // Refetch wishlist with populated products
146
+ return await this.findUserWishlist(userId);
147
+ } catch (error) {
148
+ throw new Error(`Failed to remove product from wishlist: ${error.message}`);
149
+ }
150
+ },
151
+
152
+ async clearWishlist(userId) {
153
+ try {
154
+ const wishlist = await this.findUserWishlist(userId);
155
+
156
+ if (!wishlist) {
157
+ throw new Error('Wishlist not found');
158
+ }
159
+
160
+ await strapi.entityService.update('plugin::webbycommerce.wishlist', wishlist.id, {
161
+ data: {
162
+ products: { set: [] },
163
+ },
164
+ });
165
+
166
+ // Refetch wishlist with populated products
167
+ return await this.findUserWishlist(userId);
168
+ } catch (error) {
169
+ throw new Error(`Failed to clear wishlist: ${error.message}`);
170
+ }
171
+ },
172
+
173
+ async updateWishlist(userId, data) {
174
+ try {
175
+ const wishlist = await this.findUserWishlist(userId);
176
+
177
+ if (!wishlist) {
178
+ throw new Error('Wishlist not found');
179
+ }
180
+
181
+ await strapi.entityService.update('plugin::webbycommerce.wishlist', wishlist.id, {
182
+ data: {
183
+ name: data.name !== undefined ? data.name : wishlist.name,
184
+ description: data.description !== undefined ? data.description : wishlist.description,
185
+ isPublic: data.isPublic !== undefined ? data.isPublic : wishlist.isPublic,
186
+ },
187
+ });
188
+
189
+ // Refetch wishlist with populated products
190
+ return await this.findUserWishlist(userId);
191
+ } catch (error) {
192
+ throw new Error(`Failed to update wishlist: ${error.message}`);
193
+ }
194
+ },
195
+
196
+ async getWishlistStats(userId) {
197
+ try {
198
+ const wishlist = await this.findUserWishlist(userId);
199
+
200
+ if (!wishlist) {
201
+ return {
202
+ totalProducts: 0,
203
+ totalValue: 0,
204
+ categories: [],
205
+ };
206
+ }
207
+
208
+ // Ensure products array exists
209
+ if (!wishlist.products) {
210
+ wishlist.products = [];
211
+ }
212
+
213
+ const totalProducts = wishlist.products.length;
214
+ const totalValue = wishlist.products.reduce((sum, product) => {
215
+ return sum + (product.price || 0);
216
+ }, 0);
217
+
218
+ const categoryCounts = {};
219
+ wishlist.products.forEach(product => {
220
+ const categoryName = product.product_categories?.[0]?.name || 'Uncategorized';
221
+ categoryCounts[categoryName] = (categoryCounts[categoryName] || 0) + 1;
222
+ });
223
+
224
+ const categories = Object.entries(categoryCounts).map(([name, count]) => ({
225
+ name,
226
+ count,
227
+ }));
228
+
229
+ return {
230
+ totalProducts,
231
+ totalValue,
232
+ categories,
233
+ };
234
+ } catch (error) {
235
+ throw new Error(`Failed to get wishlist stats: ${error.message}`);
236
+ }
237
+ },
238
+ }));
@@ -0,0 +1,204 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'webbycommerce';
4
+ const ECOMMERCE_ACTION = 'plugin::webbycommerce.ecommerce.enable';
5
+ const PATCHED_FLAG = Symbol('webbycommerce-permissions-patched');
6
+ const SETTINGS_KEY = 'settings';
7
+
8
+ const getStore = () => {
9
+ return strapi.store({ type: 'plugin', name: PLUGIN_ID });
10
+ };
11
+
12
+ const loadAllowedOrigins = async () => {
13
+ const store = getStore();
14
+ const value = (await store.get({ key: SETTINGS_KEY })) || {};
15
+ const input = Array.isArray(value.allowedOrigins) ? value.allowedOrigins : [];
16
+
17
+ return input
18
+ .map((value) => (typeof value === 'string' ? value.trim() : ''))
19
+ .filter((value) => value.length > 0)
20
+ .map((value) => value.toLowerCase().replace(/\/+$/, ''));
21
+ };
22
+
23
+ const isOriginAllowed = async (ctx) => {
24
+ const allowedOrigins = await loadAllowedOrigins();
25
+
26
+ // If no origins are configured, allow all
27
+ if (!allowedOrigins.length) {
28
+ return true;
29
+ }
30
+
31
+ const originHeader = ctx.request.header?.origin || '';
32
+ const hostHeader = ctx.request.header?.host || '';
33
+ let hostname = '';
34
+
35
+ if (originHeader) {
36
+ try {
37
+ const parsed = new URL(originHeader);
38
+ hostname = parsed.hostname.toLowerCase();
39
+ } catch {
40
+ // ignore invalid origin header
41
+ }
42
+ }
43
+
44
+ if (!hostname && hostHeader) {
45
+ hostname = hostHeader.split(':')[0].toLowerCase();
46
+ }
47
+
48
+ if (!hostname) {
49
+ return false;
50
+ }
51
+
52
+ const normalized = (value) =>
53
+ value
54
+ .toLowerCase()
55
+ .replace(/^https?:\/\//, '')
56
+ .split('/')[0]
57
+ .split(':')[0];
58
+
59
+ const requestHost = hostname.split(':')[0];
60
+
61
+ return allowedOrigins.some((origin) => normalized(origin) === requestHost);
62
+ };
63
+
64
+ const patchUsersPermissionsGetActions = (usersPermissionsService) => {
65
+ if (!usersPermissionsService || usersPermissionsService[PATCHED_FLAG]) {
66
+ return;
67
+ }
68
+
69
+ const originalGetActions = usersPermissionsService.getActions.bind(usersPermissionsService);
70
+
71
+ usersPermissionsService.getActions = (options = {}) => {
72
+ const result = originalGetActions(options) || {};
73
+ const defaultEnable = options?.defaultEnable ?? false;
74
+ const namespace = `plugin::${PLUGIN_ID}`;
75
+
76
+ const currentControllers = result[namespace]?.controllers || {};
77
+ const patchedControllers = { ...currentControllers };
78
+
79
+ // Register ecommerce enable action
80
+ const controllerActions = patchedControllers.ecommerce ? { ...patchedControllers.ecommerce } : {};
81
+ if (!controllerActions.enable) {
82
+ controllerActions.enable = { enabled: defaultEnable, policy: '' };
83
+ }
84
+ patchedControllers.ecommerce = controllerActions;
85
+
86
+ result[namespace] = {
87
+ controllers: patchedControllers,
88
+ };
89
+
90
+ return result;
91
+ };
92
+
93
+ usersPermissionsService[PATCHED_FLAG] = true;
94
+ };
95
+
96
+ const registerEcommerceActions = async () => {
97
+ const permissionsService = strapi.plugin('users-permissions')?.service('users-permissions');
98
+
99
+ if (!permissionsService || typeof permissionsService.syncPermissions !== 'function') {
100
+ return;
101
+ }
102
+
103
+ patchUsersPermissionsGetActions(permissionsService);
104
+
105
+ // Sync ensures the action exists in the Users & Permissions schema
106
+ await permissionsService.syncPermissions();
107
+ };
108
+
109
+ const getRoleIdFromCtx = async (ctx) => {
110
+ if (ctx.state?.user?.role?.id) {
111
+ return ctx.state.user.role.id;
112
+ }
113
+
114
+ if (ctx.state?.auth?.credentials?.role?.id) {
115
+ return ctx.state.auth.credentials.role.id;
116
+ }
117
+
118
+ if (ctx.state?.auth?.strategy?.name === 'api-token') {
119
+ // API tokens are handled separately via the permissions array on the token
120
+ return null;
121
+ }
122
+
123
+ const publicRole = await strapi.db.query('plugin::users-permissions.role').findOne({
124
+ where: { type: 'public' },
125
+ select: ['id'],
126
+ });
127
+
128
+ return publicRole?.id || null;
129
+ };
130
+
131
+ const hasApiTokenPermission = (ctx, action) => {
132
+ if (ctx.state?.auth?.strategy?.name !== 'api-token') {
133
+ return false;
134
+ }
135
+
136
+ const permissions = ctx.state?.auth?.credentials?.permissions;
137
+ if (!Array.isArray(permissions)) {
138
+ return false;
139
+ }
140
+
141
+ return permissions.includes(action);
142
+ };
143
+
144
+ const isActionEnabledForRole = (rolePermissions, action) => {
145
+ if (!rolePermissions) {
146
+ return false;
147
+ }
148
+
149
+ // Action format for plugins:
150
+ // plugin::webbycommerce.ecommerce.enable
151
+ // namespace: plugin::webbycommerce
152
+ // controller: ecommerce
153
+ // actionName: enable
154
+ const [namespace, controller, actionName] = action.split('.');
155
+
156
+ const controllerSet = rolePermissions?.[namespace]?.controllers?.[controller];
157
+ return controllerSet?.[actionName]?.enabled === true;
158
+ };
159
+
160
+ const ensureEcommercePermission = async (ctx) => {
161
+ const action = ECOMMERCE_ACTION;
162
+
163
+ // First, enforce allowed origin settings (if any are configured)
164
+ const originOk = await isOriginAllowed(ctx);
165
+ if (!originOk) {
166
+ ctx.unauthorized('Ecommerce facility is not enabled for this origin');
167
+ return false;
168
+ }
169
+
170
+ if (hasApiTokenPermission(ctx, action)) {
171
+ return true;
172
+ }
173
+
174
+ const roleId = await getRoleIdFromCtx(ctx);
175
+
176
+ if (!roleId) {
177
+ ctx.unauthorized('Ecommerce facility is not enabled for this role');
178
+ return false;
179
+ }
180
+
181
+ // Check plugin permission via Users & Permissions permissions table
182
+ const permissions = await strapi.db
183
+ .query('plugin::users-permissions.permission')
184
+ .findMany({
185
+ where: {
186
+ action,
187
+ role: roleId,
188
+ },
189
+ });
190
+
191
+ if (Array.isArray(permissions) && permissions.length > 0) {
192
+ return true;
193
+ }
194
+
195
+ ctx.unauthorized('Ecommerce facility is not enabled for this role');
196
+ return false;
197
+ };
198
+
199
+ module.exports = {
200
+ ECOMMERCE_ACTION,
201
+ registerEcommerceActions,
202
+ ensureEcommercePermission,
203
+ };
204
+
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extend the users-permissions user schema with OTP fields
5
+ * This function adds 'otp' and 'isOtpVerified' fields to the user table
6
+ */
7
+ async function extendUserSchemaWithOtpFields(strapi) {
8
+ try {
9
+ const db = strapi.db;
10
+ const client = db.config.connection.client;
11
+ const tableName = 'up_users'; // Strapi's default users table name
12
+
13
+ // Check if database columns already exist by querying the database directly
14
+ let fieldsExist = false;
15
+ try {
16
+ const connection = db.connection;
17
+ const knex = connection;
18
+
19
+ // Check if columns exist in the database
20
+ if (client === 'postgres') {
21
+ const result = await knex.raw(`
22
+ SELECT column_name
23
+ FROM information_schema.columns
24
+ WHERE table_name='${tableName}'
25
+ AND (column_name='otp' OR column_name='is_otp_verified')
26
+ `);
27
+ fieldsExist = result.rows.length >= 2;
28
+ } else if (client === 'mysql' || client === 'mysql2') {
29
+ const result = await knex.raw(`
30
+ SELECT COLUMN_NAME
31
+ FROM INFORMATION_SCHEMA.COLUMNS
32
+ WHERE TABLE_SCHEMA = DATABASE()
33
+ AND TABLE_NAME = '${tableName}'
34
+ AND (COLUMN_NAME = 'otp' OR COLUMN_NAME = 'is_otp_verified')
35
+ `);
36
+ fieldsExist = result[0].length >= 2;
37
+ } else {
38
+ // SQLite
39
+ const tableInfo = await knex.raw(`PRAGMA table_info(${tableName})`);
40
+ const columns = tableInfo.map((col) => col.name);
41
+ fieldsExist = columns.includes('otp') && columns.includes('is_otp_verified');
42
+ }
43
+ } catch (err) {
44
+ fieldsExist = false;
45
+ }
46
+
47
+ if (fieldsExist) {
48
+ strapi.log.info('[webbycommerce] OTP fields already exist in database');
49
+ return true;
50
+ }
51
+
52
+ strapi.log.info('[webbycommerce] OTP fields not found, adding them to user schema...');
53
+
54
+ // Get the database connection
55
+ const connection = db.connection;
56
+ const knex = connection;
57
+
58
+ // Add columns based on database type
59
+ let otpAdded = false;
60
+ let isOtpVerifiedAdded = false;
61
+
62
+ if (client === 'sqlite' || client === 'sqlite3') {
63
+ // SQLite doesn't support ALTER TABLE ADD COLUMN IF NOT EXISTS directly
64
+ // We need to check if column exists first
65
+ const tableInfo = await knex.raw(`PRAGMA table_info(${tableName})`);
66
+ const columns = tableInfo.map((col) => col.name);
67
+
68
+ if (!columns.includes('otp')) {
69
+ await knex.schema.alterTable(tableName, (table) => {
70
+ table.integer('otp').nullable();
71
+ });
72
+ otpAdded = true;
73
+ strapi.log.info('[webbycommerce] Added "otp" column to user table');
74
+ }
75
+
76
+ if (!columns.includes('is_otp_verified')) {
77
+ await knex.schema.alterTable(tableName, (table) => {
78
+ table.boolean('is_otp_verified').defaultTo(false);
79
+ });
80
+ isOtpVerifiedAdded = true;
81
+ strapi.log.info('[webbycommerce] Added "is_otp_verified" column to user table');
82
+ }
83
+ } else if (client === 'postgres') {
84
+ // PostgreSQL
85
+ const otpExists = await knex.raw(`
86
+ SELECT column_name
87
+ FROM information_schema.columns
88
+ WHERE table_name='${tableName}' AND column_name='otp'
89
+ `);
90
+
91
+ if (otpExists.rows.length === 0) {
92
+ await knex.raw(`ALTER TABLE ${tableName} ADD COLUMN otp INTEGER`);
93
+ otpAdded = true;
94
+ strapi.log.info('[webbycommerce] Added "otp" column to user table');
95
+ }
96
+
97
+ const isOtpVerifiedExists = await knex.raw(`
98
+ SELECT column_name
99
+ FROM information_schema.columns
100
+ WHERE table_name='${tableName}' AND column_name='is_otp_verified'
101
+ `);
102
+
103
+ if (isOtpVerifiedExists.rows.length === 0) {
104
+ await knex.raw(`ALTER TABLE ${tableName} ADD COLUMN is_otp_verified BOOLEAN DEFAULT false`);
105
+ isOtpVerifiedAdded = true;
106
+ strapi.log.info('[webbycommerce] Added "is_otp_verified" column to user table');
107
+ }
108
+ } else if (client === 'mysql' || client === 'mysql2') {
109
+ // MySQL
110
+ const otpExists = await knex.raw(`
111
+ SELECT COLUMN_NAME
112
+ FROM INFORMATION_SCHEMA.COLUMNS
113
+ WHERE TABLE_SCHEMA = DATABASE()
114
+ AND TABLE_NAME = '${tableName}'
115
+ AND COLUMN_NAME = 'otp'
116
+ `);
117
+
118
+ if (otpExists[0].length === 0) {
119
+ await knex.raw(`ALTER TABLE \`${tableName}\` ADD COLUMN \`otp\` INT NULL`);
120
+ otpAdded = true;
121
+ strapi.log.info('[webbycommerce] Added "otp" column to user table');
122
+ }
123
+
124
+ const isOtpVerifiedExists = await knex.raw(`
125
+ SELECT COLUMN_NAME
126
+ FROM INFORMATION_SCHEMA.COLUMNS
127
+ WHERE TABLE_SCHEMA = DATABASE()
128
+ AND TABLE_NAME = '${tableName}'
129
+ AND COLUMN_NAME = 'is_otp_verified'
130
+ `);
131
+
132
+ if (isOtpVerifiedExists[0].length === 0) {
133
+ await knex.raw(`ALTER TABLE \`${tableName}\` ADD COLUMN \`is_otp_verified\` BOOLEAN DEFAULT false`);
134
+ isOtpVerifiedAdded = true;
135
+ strapi.log.info('[webbycommerce] Added "is_otp_verified" column to user table');
136
+ }
137
+ } else {
138
+ strapi.log.warn(
139
+ `[webbycommerce] Database client "${client}" not supported for automatic schema extension. Please manually add OTP fields to user schema.`
140
+ );
141
+ return false;
142
+ }
143
+
144
+ // Note: We don't modify the content-type schema at runtime as it breaks the admin panel
145
+ // The raw SQL queries in the controller will work with the database columns directly
146
+ // If you need the fields to appear in the admin panel, create a schema extension file
147
+ // in your main Strapi project: src/extensions/users-permissions/content-types/user/schema.json
148
+
149
+ strapi.log.info('[webbycommerce] User schema extension completed successfully');
150
+ return true;
151
+ } catch (error) {
152
+ strapi.log.error('[webbycommerce] Failed to extend user schema with OTP fields:', error);
153
+ strapi.log.error('[webbycommerce] Error details:', error.message);
154
+ strapi.log.error(
155
+ '[webbycommerce] Please manually extend the user schema by creating a schema extension file in your Strapi project.'
156
+ );
157
+ return false;
158
+ }
159
+ }
160
+
161
+ module.exports = { extendUserSchemaWithOtpFields };