Files
orion/static/shared/js/api-client.js
Samir Boulahtit 559be59412 feat: add customer token support to API client
Extended path-based token selection to include customer tokens:
- /shop/* routes now use customer_token
- Fallback order: admin_token || vendor_token || customer_token

This ensures the correct token is used when logged into multiple
portals (admin, vendor, customer) simultaneously.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 12:53:01 +01:00

433 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// static/shared/js/api-client.js
// noqa: js-001 - Core infrastructure, uses its own logger (apiLog)
/**
* 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
*
* Uses path-based detection to return the correct token:
* - /admin/* routes use admin_token
* - /vendor/* routes use vendor_token
* - /shop/* routes use customer_token
* - Other routes fall back to admin_token || vendor_token || customer_token
*/
getToken() {
const adminToken = localStorage.getItem('admin_token');
const vendorToken = localStorage.getItem('vendor_token');
const customerToken = localStorage.getItem('customer_token');
const currentPath = window.location.pathname;
let token;
let source;
// Path-based token selection
if (currentPath.startsWith('/vendor/') || currentPath.startsWith('/api/v1/vendor/')) {
token = vendorToken;
source = 'vendor (path-based)';
} else if (currentPath.startsWith('/admin/') || currentPath.startsWith('/api/v1/admin/')) {
token = adminToken;
source = 'admin (path-based)';
} else if (currentPath.includes('/shop/') || currentPath.startsWith('/api/v1/shop/')) {
token = customerToken;
source = 'customer (path-based)';
} else {
// Default fallback for other paths
token = adminToken || vendorToken || customerToken;
source = token === adminToken ? 'admin (fallback)' :
token === vendorToken ? 'vendor (fallback)' : 'customer (fallback)';
}
apiLog.debug('Getting token:', {
hasAdminToken: !!adminToken,
hasVendorToken: !!vendorToken,
hasCustomerToken: !!customerToken,
currentPath,
source,
usingToken: token ? source : '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');
}
};
// 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');