// static/shared/js/api-client.js /** * API Client for Multi-Tenant Ecommerce Platform * * Provides utilities for: * - Making authenticated API calls * - Token management * - Error handling * - Request/response interceptors */ // Log levels: 0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug const API_LOG_LEVEL = 3; // Set to 3 for production, 4 for full debugging const apiLog = { error: (...args) => API_LOG_LEVEL >= 1 && console.error('❌ [API ERROR]', ...args), warn: (...args) => API_LOG_LEVEL >= 2 && console.warn('⚠️ [API WARN]', ...args), info: (...args) => API_LOG_LEVEL >= 3 && console.info('ℹ️ [API INFO]', ...args), debug: (...args) => API_LOG_LEVEL >= 4 && console.log('🔍 [API DEBUG]', ...args) }; const API_BASE_URL = '/api/v1'; /** * API Client Class */ class APIClient { constructor(baseURL = API_BASE_URL) { this.baseURL = baseURL; apiLog.info('API Client initialized with base URL:', baseURL); } /** * Get stored authentication token */ getToken() { const adminToken = localStorage.getItem('admin_token'); const vendorToken = localStorage.getItem('vendor_token'); const token = adminToken || vendorToken; apiLog.debug('Getting token:', { hasAdminToken: !!adminToken, hasVendorToken: !!vendorToken, usingToken: token ? 'admin or vendor' : 'none' }); return token; } /** * Get default headers with authentication */ getHeaders(additionalHeaders = {}) { const headers = { 'Content-Type': 'application/json', ...additionalHeaders }; const token = this.getToken(); if (token) { headers['Authorization'] = `Bearer ${token}`; apiLog.debug('Authorization header added'); } else { apiLog.debug('No token available, request will be unauthenticated'); } return headers; } /** * Make API request */ async request(endpoint, options = {}) { const url = `${this.baseURL}${endpoint}`; const method = options.method || 'GET'; apiLog.info(`${method} ${url}`); apiLog.debug('Request options:', { method, hasBody: !!options.body, customHeaders: Object.keys(options.headers || {}) }); const config = { ...options, headers: this.getHeaders(options.headers) }; try { const startTime = Date.now(); const response = await fetch(url, config); const duration = Date.now() - startTime; apiLog.info(`Response: ${response.status} ${response.statusText} (${duration}ms)`); // Parse response let data; try { data = await response.json(); apiLog.debug('Response data received:', { hasData: !!data, dataType: typeof data, keys: data ? Object.keys(data) : [] }); } catch (parseError) { apiLog.error('Failed to parse JSON response:', parseError); throw new Error('Invalid JSON response from server'); } // Handle 401 Unauthorized - Just clear tokens, DON'T redirect if (response.status === 401) { apiLog.warn('401 Unauthorized - Authentication failed'); apiLog.debug('Error details:', data); apiLog.info('Clearing authentication tokens'); this.clearTokens(); const errorMessage = data.message || data.detail || 'Unauthorized - please login again'; apiLog.error('Throwing authentication error:', errorMessage); throw new Error(errorMessage); } // Handle non-OK responses if (!response.ok) { const errorMessage = data.detail || data.message || `Request failed with status ${response.status}`; apiLog.error('Request failed:', { status: response.status, message: errorMessage, errorCode: data.error_code }); throw new Error(errorMessage); } apiLog.info('Request completed successfully'); return data; } catch (error) { // Log error details if (error.name === 'TypeError' && error.message.includes('fetch')) { apiLog.error('Network error - Failed to connect to server'); } else { apiLog.error('Request error:', error.message); } apiLog.debug('Full error:', error); throw error; } } /** * GET request */ async get(endpoint, params = {}) { const queryString = new URLSearchParams(params).toString(); const url = queryString ? `${endpoint}?${queryString}` : endpoint; apiLog.debug('GET request params:', params); return this.request(url, { method: 'GET' }); } /** * POST request */ async post(endpoint, data = {}) { apiLog.debug('POST request data:', { hasData: !!data, dataKeys: Object.keys(data) }); return this.request(endpoint, { method: 'POST', body: JSON.stringify(data) }); } /** * PUT request */ async put(endpoint, data = {}) { apiLog.debug('PUT request data:', { hasData: !!data, dataKeys: Object.keys(data) }); return this.request(endpoint, { method: 'PUT', body: JSON.stringify(data) }); } /** * DELETE request */ async delete(endpoint) { apiLog.debug('DELETE request'); return this.request(endpoint, { method: 'DELETE' }); } /** * Clear authentication tokens */ clearTokens() { apiLog.info('Clearing all authentication tokens...'); const tokensBefore = { admin_token: !!localStorage.getItem('admin_token'), admin_user: !!localStorage.getItem('admin_user'), vendor_token: !!localStorage.getItem('vendor_token'), vendor_user: !!localStorage.getItem('vendor_user'), token: !!localStorage.getItem('token') }; apiLog.debug('Tokens before clear:', tokensBefore); localStorage.removeItem('admin_token'); localStorage.removeItem('admin_user'); localStorage.removeItem('vendor_token'); localStorage.removeItem('vendor_user'); localStorage.removeItem('token'); const tokensAfter = { admin_token: !!localStorage.getItem('admin_token'), admin_user: !!localStorage.getItem('admin_user'), vendor_token: !!localStorage.getItem('vendor_token'), vendor_user: !!localStorage.getItem('vendor_user'), token: !!localStorage.getItem('token') }; apiLog.debug('Tokens after clear:', tokensAfter); apiLog.info('All tokens cleared'); } /** * Handle unauthorized access * DEPRECATED - Now just clears tokens, doesn't redirect * Server handles redirects via exception handler */ handleUnauthorized() { apiLog.warn('handleUnauthorized called (DEPRECATED) - use clearTokens instead'); this.clearTokens(); } } // Create global API client instance const apiClient = new APIClient(); apiLog.info('Global API client created'); /** * Authentication helpers */ const Auth = { /** * Check if user is authenticated */ isAuthenticated() { const token = localStorage.getItem('admin_token') || localStorage.getItem('vendor_token'); const isAuth = !!token; apiLog.debug('Auth check:', isAuth ? 'authenticated' : 'not authenticated'); return isAuth; }, /** * Get current user */ getCurrentUser() { const userStr = localStorage.getItem('admin_user') || localStorage.getItem('vendor_user'); if (!userStr) { apiLog.debug('No user found in storage'); return null; } try { const user = JSON.parse(userStr); apiLog.debug('Current user:', { username: user.username, role: user.role, id: user.id }); return user; } catch (e) { apiLog.error('Failed to parse user data:', e); return null; } }, /** * Check if user is admin */ isAdmin() { const user = this.getCurrentUser(); const isAdmin = user && user.role === 'admin'; apiLog.debug('Admin check:', isAdmin ? 'is admin' : 'not admin'); return isAdmin; }, /** * Login */ async login(username, password) { apiLog.info('Auth.login called'); const response = await apiClient.post('/auth/login', { username, password }); // Store token and user if (response.user.role === 'admin') { apiLog.info('Storing admin credentials'); localStorage.setItem('admin_token', response.access_token); localStorage.setItem('admin_user', JSON.stringify(response.user)); } else { apiLog.info('Storing vendor credentials'); localStorage.setItem('vendor_token', response.access_token); localStorage.setItem('vendor_user', JSON.stringify(response.user)); } return response; }, /** * Logout */ logout() { apiLog.info('Auth.logout called'); apiClient.clearTokens(); apiLog.info('User logged out'); } }; /** * Utility functions */ const Utils = { /** * Format date */ formatDate(dateString) { if (!dateString) return '-'; const date = new Date(dateString); return date.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); }, /** * Format datetime */ formatDateTime(dateString) { if (!dateString) return '-'; const date = new Date(dateString); return date.toLocaleString('en-GB', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }); }, /** * Format currency */ formatCurrency(amount, currency = 'EUR') { if (amount === null || amount === undefined) return '-'; return new Intl.NumberFormat('en-GB', { style: 'currency', currency: currency }).format(amount); }, /** * Debounce function */ debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, /** * Show toast notification */ showToast(message, type = 'info', duration = 3000) { apiLog.debug('Showing toast:', { message, type, duration }); // Create toast element const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; // Style toast.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 16px 24px; background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'}; color: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10000; animation: slideIn 0.3s ease; max-width: 400px; `; // Add to page document.body.appendChild(toast); // Remove after duration setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease'; setTimeout(() => toast.remove(), 300); }, duration); }, /** * Confirm dialog */ async confirm(message, title = 'Confirm') { return window.confirm(`${title}\n\n${message}`); } }; // Add animation styles const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } } `; document.head.appendChild(style); // Export for use in other scripts if (typeof module !== 'undefined' && module.exports) { module.exports = { APIClient, apiClient, Auth, Utils }; } // Table scroll detection helper function initTableScrollDetection() { const observer = new MutationObserver(() => { const tables = document.querySelectorAll('.table-responsive'); tables.forEach(table => { if (!table.hasAttribute('data-scroll-initialized')) { table.setAttribute('data-scroll-initialized', 'true'); table.addEventListener('scroll', function() { if (this.scrollLeft > 0) { this.classList.add('is-scrolled'); } else { this.classList.remove('is-scrolled'); } }); // Check initial state if (table.scrollLeft > 0) { table.classList.add('is-scrolled'); } } }); }); observer.observe(document.body, { childList: true, subtree: true }); } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initTableScrollDetection); } else { initTableScrollDetection(); } apiLog.info('API Client module loaded');