504 lines
14 KiB
JavaScript
504 lines
14 KiB
JavaScript
// 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'); |