diff --git a/.architecture-rules/frontend.yaml b/.architecture-rules/frontend.yaml index 6c9782f8..cb08b41a 100644 --- a/.architecture-rules/frontend.yaml +++ b/.architecture-rules/frontend.yaml @@ -131,6 +131,42 @@ javascript_rules: - "init-alpine.js" - "login.js" + - id: "JS-014" + name: "Vendor API calls must not include vendorCode in path" + severity: "error" + description: | + Vendor API endpoints use JWT token authentication, NOT URL path parameters. + The vendor is identified from the JWT token via get_current_vendor_api dependency. + + Do NOT include vendorCode in API paths for authenticated vendor endpoints. + + WRONG (vendorCode in API path): + apiClient.get(`/vendor/${this.vendorCode}/orders`) + apiClient.post(`/vendor/${this.vendorCode}/products`, data) + + RIGHT (no vendorCode, uses JWT): + apiClient.get(`/vendor/orders`) + apiClient.post(`/vendor/products`, data) + + EXCEPTIONS (these endpoints DO use vendorCode in path): + - /vendor/{vendor_code} - Public vendor info (info.router) + - /vendor/{vendor_code}/content-pages/* - Content pages management + - Page URLs (not API calls) like window.location.href = `/vendor/${vendorCode}/...` + + Why this matters: + - Including vendorCode causes 404 errors ("/vendor/wizamart/orders" not found) + - The JWT token already identifies the vendor + - Consistent with the API design pattern + pattern: + file_pattern: "static/vendor/js/**/*.js" + anti_patterns: + - "apiClient\\.(get|post|put|delete|patch)\\s*\\(\\s*`/vendor/\\$\\{this\\.vendorCode\\}/(orders|products|customers|inventory|analytics|dashboard|profile|settings|team|notifications|invoices|payments|media|marketplace|letzshop|billing|features|usage)" + exceptions: + - "init-alpine.js" + - "login.js" + - "content-pages.js" + - "content-page-edit.js" + - id: "JS-007" name: "Set loading state before async operations" severity: "warning" diff --git a/scripts/validate_architecture.py b/scripts/validate_architecture.py index a43e5a0c..d1c78f43 100755 --- a/scripts/validate_architecture.py +++ b/scripts/validate_architecture.py @@ -2865,6 +2865,9 @@ class ArchitectureValidator: # JS-013: Check that components overriding init() call parent init self._check_parent_init_call(file_path, content, lines) + # JS-014: Check that vendor API calls don't include vendorCode in path + self._check_vendor_api_paths(file_path, content, lines) + def _check_platform_settings_usage( self, file_path: Path, content: str, lines: list[str] ): @@ -3051,6 +3054,55 @@ class ArchitectureValidator: ) break + def _check_vendor_api_paths( + self, file_path: Path, content: str, lines: list[str] + ): + """ + JS-014: Check that vendor API calls don't include vendorCode in path. + + Vendor API endpoints use JWT token authentication, NOT URL path parameters. + The vendorCode is only used for page URLs (navigation), not API calls. + + Incorrect: apiClient.get(`/vendor/${this.vendorCode}/orders`) + Correct: apiClient.get(`/vendor/orders`) + + Exceptions (these DO use vendorCode in path): + - /vendor/{vendor_code} (public vendor info) + - /vendor/{vendor_code}/content-pages (public content) + """ + # Only check vendor JS files + if "/vendor/js/" not in str(file_path): + return + + # Pattern to match apiClient calls with vendorCode in the path + # Matches patterns like: + # apiClient.get(`/vendor/${this.vendorCode}/ + # apiClient.post(`/vendor/${vendorCode}/ + # apiClient.put(`/vendor/${this.vendorCode}/ + # apiClient.delete(`/vendor/${this.vendorCode}/ + pattern = r"apiClient\.(get|post|put|delete|patch)\s*\(\s*[`'\"]\/vendor\/\$\{(?:this\.)?vendorCode\}\/" + + for i, line in enumerate(lines, 1): + if re.search(pattern, line): + # Check if this is an allowed exception + # content-pages uses vendorCode for public content access + is_exception = ( + "/content-pages" in line + or "content-page" in file_path.name + ) + + if not is_exception: + self._add_violation( + rule_id="JS-014", + rule_name="Vendor API calls must not include vendorCode in path", + severity=Severity.ERROR, + file_path=file_path, + line_number=i, + message="Vendor API endpoints use JWT authentication, not URL path parameters", + context=line.strip()[:100], + suggestion="Remove vendorCode from path: /vendor/orders instead of /vendor/${this.vendorCode}/orders", + ) + def _validate_templates(self, target_path: Path): """Validate template patterns""" print("📄 Validating templates...") diff --git a/static/vendor/js/analytics.js b/static/vendor/js/analytics.js index 5eb2ae4d..12b70cdb 100644 --- a/static/vendor/js/analytics.js +++ b/static/vendor/js/analytics.js @@ -107,7 +107,7 @@ function vendorAnalytics() { */ async fetchAnalytics() { try { - const response = await apiClient.get(`/vendor/${this.vendorCode}/analytics?period=${this.period}`); + const response = await apiClient.get(`/vendor/analytics?period=${this.period}`); return response; } catch (error) { // Analytics might require feature access @@ -124,7 +124,7 @@ function vendorAnalytics() { */ async fetchStats() { try { - const response = await apiClient.get(`/vendor/${this.vendorCode}/dashboard/stats`); + const response = await apiClient.get(`/vendor/dashboard/stats`); return { total_products: response.catalog?.total_products || 0, active_products: response.catalog?.active_products || 0, diff --git a/static/vendor/js/customers.js b/static/vendor/js/customers.js index 0676143f..afb1fc65 100644 --- a/static/vendor/js/customers.js +++ b/static/vendor/js/customers.js @@ -148,7 +148,7 @@ function vendorCustomers() { params.append('status', this.filters.status); } - const response = await apiClient.get(`/vendor/${this.vendorCode}/customers?${params.toString()}`); + const response = await apiClient.get(`/vendor/customers?${params.toString()}`); this.customers = response.customers || []; this.pagination.total = response.total || 0; @@ -212,7 +212,7 @@ function vendorCustomers() { async viewCustomer(customer) { this.loading = true; try { - const response = await apiClient.get(`/vendor/${this.vendorCode}/customers/${customer.id}`); + const response = await apiClient.get(`/vendor/customers/${customer.id}`); this.selectedCustomer = response; this.showDetailModal = true; vendorCustomersLog.info('Loaded customer details:', customer.id); @@ -230,7 +230,7 @@ function vendorCustomers() { async viewCustomerOrders(customer) { this.loading = true; try { - const response = await apiClient.get(`/vendor/${this.vendorCode}/customers/${customer.id}/orders`); + const response = await apiClient.get(`/vendor/customers/${customer.id}/orders`); this.selectedCustomer = customer; this.customerOrders = response.orders || []; this.showOrdersModal = true; diff --git a/static/vendor/js/inventory.js b/static/vendor/js/inventory.js index a184a1f9..6cbcabf5 100644 --- a/static/vendor/js/inventory.js +++ b/static/vendor/js/inventory.js @@ -182,7 +182,7 @@ function vendorInventory() { params.append('low_stock', this.filters.low_stock); } - const response = await apiClient.get(`/vendor/${this.vendorCode}/inventory?${params.toString()}`); + const response = await apiClient.get(`/vendor/inventory?${params.toString()}`); this.inventory = response.items || []; this.pagination.total = response.total || 0; @@ -286,7 +286,7 @@ function vendorInventory() { this.saving = true; try { - await apiClient.post(`/vendor/${this.vendorCode}/inventory/adjust`, { + await apiClient.post(`/vendor/inventory/adjust`, { product_id: this.selectedItem.product_id, location: this.selectedItem.location, quantity: this.adjustForm.quantity, @@ -317,7 +317,7 @@ function vendorInventory() { this.saving = true; try { - await apiClient.post(`/vendor/${this.vendorCode}/inventory/set`, { + await apiClient.post(`/vendor/inventory/set`, { product_id: this.selectedItem.product_id, location: this.selectedItem.location, quantity: this.setForm.quantity @@ -452,7 +452,7 @@ function vendorInventory() { const item = this.inventory.find(i => i.id === itemId); if (item) { try { - await apiClient.post(`/vendor/${this.vendorCode}/inventory/adjust`, { + await apiClient.post(`/vendor/inventory/adjust`, { product_id: item.product_id, location: item.location, quantity: this.bulkAdjustForm.quantity, diff --git a/static/vendor/js/notifications.js b/static/vendor/js/notifications.js index 0037288b..1b6e31c6 100644 --- a/static/vendor/js/notifications.js +++ b/static/vendor/js/notifications.js @@ -95,7 +95,7 @@ function vendorNotifications() { params.append('unread_only', 'true'); } - const response = await apiClient.get(`/vendor/${this.vendorCode}/notifications?${params}`); + const response = await apiClient.get(`/vendor/notifications?${params}`); this.notifications = response.notifications || []; this.stats.total = response.total || 0; @@ -115,7 +115,7 @@ function vendorNotifications() { */ async markAsRead(notification) { try { - await apiClient.put(`/vendor/${this.vendorCode}/notifications/${notification.id}/read`); + await apiClient.put(`/vendor/notifications/${notification.id}/read`); // Update local state notification.is_read = true; @@ -133,7 +133,7 @@ function vendorNotifications() { */ async markAllAsRead() { try { - await apiClient.put(`/vendor/${this.vendorCode}/notifications/mark-all-read`); + await apiClient.put(`/vendor/notifications/mark-all-read`); // Update local state this.notifications.forEach(n => n.is_read = true); @@ -155,7 +155,7 @@ function vendorNotifications() { } try { - await apiClient.delete(`/vendor/${this.vendorCode}/notifications/${notificationId}`); + await apiClient.delete(`/vendor/notifications/${notificationId}`); // Remove from local state const wasUnread = this.notifications.find(n => n.id === notificationId && !n.is_read); @@ -177,7 +177,7 @@ function vendorNotifications() { */ async openSettingsModal() { try { - const response = await apiClient.get(`/vendor/${this.vendorCode}/notifications/settings`); + const response = await apiClient.get(`/vendor/notifications/settings`); this.settingsForm = { email_notifications: response.email_notifications !== false, in_app_notifications: response.in_app_notifications !== false @@ -194,7 +194,7 @@ function vendorNotifications() { */ async saveSettings() { try { - await apiClient.put(`/vendor/${this.vendorCode}/notifications/settings`, this.settingsForm); + await apiClient.put(`/vendor/notifications/settings`, this.settingsForm); Utils.showToast('Notification settings saved', 'success'); this.showSettingsModal = false; } catch (error) { diff --git a/static/vendor/js/order-detail.js b/static/vendor/js/order-detail.js index 2042dcee..66edf3f1 100644 --- a/static/vendor/js/order-detail.js +++ b/static/vendor/js/order-detail.js @@ -94,7 +94,7 @@ function vendorOrderDetail() { try { // Load order details const orderResponse = await apiClient.get( - `/vendor/${this.vendorCode}/orders/${this.orderId}` + `/vendor/orders/${this.orderId}` ); this.order = orderResponse; this.newStatus = this.order.status; @@ -121,7 +121,7 @@ function vendorOrderDetail() { async loadShipmentStatus() { try { const response = await apiClient.get( - `/vendor/${this.vendorCode}/orders/${this.orderId}/shipment-status` + `/vendor/orders/${this.orderId}/shipment-status` ); this.shipmentStatus = response; orderDetailLog.info('Loaded shipment status:', response); @@ -138,7 +138,7 @@ function vendorOrderDetail() { try { // Search for invoices linked to this order const response = await apiClient.get( - `/vendor/${this.vendorCode}/invoices?order_id=${this.orderId}&limit=1` + `/vendor/invoices?order_id=${this.orderId}&limit=1` ); if (response.invoices && response.invoices.length > 0) { this.invoice = response.invoices[0]; @@ -223,7 +223,7 @@ function vendorOrderDetail() { } await apiClient.put( - `/vendor/${this.vendorCode}/orders/${this.orderId}/status`, + `/vendor/orders/${this.orderId}/status`, payload ); @@ -255,7 +255,7 @@ function vendorOrderDetail() { this.saving = true; try { await apiClient.post( - `/vendor/${this.vendorCode}/orders/${this.orderId}/items/${itemId}/ship`, + `/vendor/orders/${this.orderId}/items/${itemId}/ship`, {} ); @@ -281,7 +281,7 @@ function vendorOrderDetail() { for (const item of unshippedItems) { await apiClient.post( - `/vendor/${this.vendorCode}/orders/${this.orderId}/items/${item.item_id}/ship`, + `/vendor/orders/${this.orderId}/items/${item.item_id}/ship`, {} ); } @@ -294,7 +294,7 @@ function vendorOrderDetail() { } await apiClient.put( - `/vendor/${this.vendorCode}/orders/${this.orderId}/status`, + `/vendor/orders/${this.orderId}/status`, payload ); @@ -319,7 +319,7 @@ function vendorOrderDetail() { this.creatingInvoice = true; try { const response = await apiClient.post( - `/vendor/${this.vendorCode}/invoices`, + `/vendor/invoices`, { order_id: this.orderId } ); diff --git a/static/vendor/js/orders.js b/static/vendor/js/orders.js index e9668715..899daa1d 100644 --- a/static/vendor/js/orders.js +++ b/static/vendor/js/orders.js @@ -185,7 +185,7 @@ function vendorOrders() { params.append('date_to', this.filters.date_to); } - const response = await apiClient.get(`/vendor/${this.vendorCode}/orders?${params.toString()}`); + const response = await apiClient.get(`/vendor/orders?${params.toString()}`); this.orders = response.orders || []; this.pagination.total = response.total || 0; @@ -273,7 +273,7 @@ function vendorOrders() { this.saving = true; try { - await apiClient.put(`/vendor/${this.vendorCode}/orders/${this.selectedOrder.id}/status`, { + await apiClient.put(`/vendor/orders/${this.selectedOrder.id}/status`, { status: this.newStatus }); @@ -423,7 +423,7 @@ function vendorOrders() { let successCount = 0; for (const orderId of this.selectedOrders) { try { - await apiClient.put(`/vendor/${this.vendorCode}/orders/${orderId}/status`, { + await apiClient.put(`/vendor/orders/${orderId}/status`, { status: this.bulkStatus }); successCount++; diff --git a/static/vendor/js/products.js b/static/vendor/js/products.js index 1949dff9..9f017333 100644 --- a/static/vendor/js/products.js +++ b/static/vendor/js/products.js @@ -166,7 +166,7 @@ function vendorProducts() { params.append('is_featured', this.filters.featured === 'true'); } - const response = await apiClient.get(`/vendor/${this.vendorCode}/products?${params.toString()}`); + const response = await apiClient.get(`/vendor/products?${params.toString()}`); this.products = response.products || []; this.pagination.total = response.total || 0; @@ -227,7 +227,7 @@ function vendorProducts() { async toggleActive(product) { this.saving = true; try { - await apiClient.put(`/vendor/${this.vendorCode}/products/${product.id}/toggle-active`); + await apiClient.put(`/vendor/products/${product.id}/toggle-active`); product.is_active = !product.is_active; Utils.showToast( product.is_active ? 'Product activated' : 'Product deactivated', @@ -248,7 +248,7 @@ function vendorProducts() { async toggleFeatured(product) { this.saving = true; try { - await apiClient.put(`/vendor/${this.vendorCode}/products/${product.id}/toggle-featured`); + await apiClient.put(`/vendor/products/${product.id}/toggle-featured`); product.is_featured = !product.is_featured; Utils.showToast( product.is_featured ? 'Product marked as featured' : 'Product unmarked as featured', @@ -287,7 +287,7 @@ function vendorProducts() { this.saving = true; try { - await apiClient.delete(`/vendor/${this.vendorCode}/products/${this.selectedProduct.id}`); + await apiClient.delete(`/vendor/products/${this.selectedProduct.id}`); Utils.showToast('Product deleted successfully', 'success'); vendorProductsLog.info('Deleted product:', this.selectedProduct.id); @@ -410,7 +410,7 @@ function vendorProducts() { for (const productId of this.selectedProducts) { const product = this.products.find(p => p.id === productId); if (product && !product.is_active) { - await apiClient.put(`/vendor/${this.vendorCode}/products/${productId}/toggle-active`); + await apiClient.put(`/vendor/products/${productId}/toggle-active`); product.is_active = true; successCount++; } @@ -438,7 +438,7 @@ function vendorProducts() { for (const productId of this.selectedProducts) { const product = this.products.find(p => p.id === productId); if (product && product.is_active) { - await apiClient.put(`/vendor/${this.vendorCode}/products/${productId}/toggle-active`); + await apiClient.put(`/vendor/products/${productId}/toggle-active`); product.is_active = false; successCount++; } @@ -466,7 +466,7 @@ function vendorProducts() { for (const productId of this.selectedProducts) { const product = this.products.find(p => p.id === productId); if (product && !product.is_featured) { - await apiClient.put(`/vendor/${this.vendorCode}/products/${productId}/toggle-featured`); + await apiClient.put(`/vendor/products/${productId}/toggle-featured`); product.is_featured = true; successCount++; } @@ -494,7 +494,7 @@ function vendorProducts() { for (const productId of this.selectedProducts) { const product = this.products.find(p => p.id === productId); if (product && product.is_featured) { - await apiClient.put(`/vendor/${this.vendorCode}/products/${productId}/toggle-featured`); + await apiClient.put(`/vendor/products/${productId}/toggle-featured`); product.is_featured = false; successCount++; } @@ -528,7 +528,7 @@ function vendorProducts() { try { let successCount = 0; for (const productId of this.selectedProducts) { - await apiClient.delete(`/vendor/${this.vendorCode}/products/${productId}`); + await apiClient.delete(`/vendor/products/${productId}`); successCount++; } Utils.showToast(`${successCount} product(s) deleted`, 'success'); diff --git a/static/vendor/js/profile.js b/static/vendor/js/profile.js index d72a7279..f62cd48e 100644 --- a/static/vendor/js/profile.js +++ b/static/vendor/js/profile.js @@ -78,7 +78,7 @@ function vendorProfile() { this.error = ''; try { - const response = await apiClient.get(`/vendor/${this.vendorCode}/profile`); + const response = await apiClient.get(`/vendor/profile`); this.profile = response; this.form = { @@ -159,7 +159,7 @@ function vendorProfile() { this.saving = true; try { - await apiClient.put(`/vendor/${this.vendorCode}/profile`, this.form); + await apiClient.put(`/vendor/profile`, this.form); Utils.showToast('Profile updated successfully', 'success'); vendorProfileLog.info('Profile updated'); diff --git a/static/vendor/js/settings.js b/static/vendor/js/settings.js index 9302b74c..34c0dc25 100644 --- a/static/vendor/js/settings.js +++ b/static/vendor/js/settings.js @@ -92,7 +92,7 @@ function vendorSettings() { this.error = ''; try { - const response = await apiClient.get(`/vendor/${this.vendorCode}/settings`); + const response = await apiClient.get(`/vendor/settings`); this.settings = response; @@ -131,7 +131,7 @@ function vendorSettings() { async saveMarketplaceSettings() { this.saving = true; try { - await apiClient.put(`/vendor/${this.vendorCode}/settings/marketplace`, this.marketplaceForm); + await apiClient.put(`/vendor/settings/marketplace`, this.marketplaceForm); Utils.showToast('Marketplace settings saved', 'success'); vendorSettingsLog.info('Marketplace settings updated'); diff --git a/static/vendor/js/team.js b/static/vendor/js/team.js index 0b18d1bd..cc325202 100644 --- a/static/vendor/js/team.js +++ b/static/vendor/js/team.js @@ -100,7 +100,7 @@ function vendorTeam() { this.error = ''; try { - const response = await apiClient.get(`/vendor/${this.vendorCode}/team/members?include_inactive=true`); + const response = await apiClient.get(`/vendor/team/members?include_inactive=true`); this.members = response.members || []; this.stats = { @@ -123,7 +123,7 @@ function vendorTeam() { */ async loadRoles() { try { - const response = await apiClient.get(`/vendor/${this.vendorCode}/team/roles`); + const response = await apiClient.get(`/vendor/team/roles`); this.roles = response.roles || []; vendorTeamLog.info('Loaded roles:', this.roles.length); } catch (error) { @@ -155,7 +155,7 @@ function vendorTeam() { this.saving = true; try { - await apiClient.post(`/vendor/${this.vendorCode}/team/invite`, this.inviteForm); + await apiClient.post(`/vendor/team/invite`, this.inviteForm); Utils.showToast('Invitation sent successfully', 'success'); vendorTeamLog.info('Invitation sent to:', this.inviteForm.email); @@ -225,7 +225,7 @@ function vendorTeam() { this.saving = true; try { - await apiClient.delete(`/vendor/${this.vendorCode}/team/members/${this.selectedMember.user_id}`); + await apiClient.delete(`/vendor/team/members/${this.selectedMember.user_id}`); Utils.showToast('Team member removed', 'success'); vendorTeamLog.info('Removed team member:', this.selectedMember.user_id);