Files
dss/admin-ui/js/services/api-client.js
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
2025-12-09 18:45:48 -03:00

269 lines
8.1 KiB
JavaScript

/**
* api-client.js
* Unified API client for backend communication
* Handles authentication, request/response formatting, error handling
* Supports incognito mode with sessionStorage fallback (lazy-initialized)
*/
class ApiClient {
constructor(baseURL = null) {
// Use relative path /api which works for both production (proxied through nginx)
// and development (proxied through Vite to backend on localhost:8002)
this.baseURL = baseURL || '/api';
this.accessToken = null;
this.refreshToken = null;
this.loadTokensFromStorage();
this.setupAuthChangeListener();
}
setupAuthChangeListener() {
// Listen for auth changes (e.g., from demo-auth-init.js or other components)
// and reload tokens from storage
document.addEventListener('auth-change', () => {
console.log('[ApiClient] Auth change detected, reloading tokens...');
this.loadTokensFromStorage();
});
window.addEventListener('auth-change', () => {
console.log('[ApiClient] Auth change detected, reloading tokens...');
this.loadTokensFromStorage();
});
}
loadTokensFromStorage() {
// Always use localStorage directly for auth tokens
// This ensures compatibility with demo-auth-init.js which also uses localStorage
const stored = localStorage.getItem('auth_tokens');
if (stored) {
try {
const parsed = JSON.parse(stored);
console.log('[ApiClient] Parsed tokens object:', { hasAccessToken: !!parsed.accessToken, hasRefreshToken: !!parsed.refreshToken });
this.accessToken = parsed.accessToken;
this.refreshToken = parsed.refreshToken;
console.log('[ApiClient] Tokens assigned - accessToken:', this.accessToken ? `${this.accessToken.substring(0, 20)}...` : 'null');
} catch (e) {
console.error('[ApiClient] Failed to parse stored tokens:', e, 'stored value:', stored);
}
} else {
console.warn('[ApiClient] No auth_tokens found in localStorage');
}
}
saveTokensToStorage() {
try {
// Always use localStorage directly for auth tokens
localStorage.setItem('auth_tokens', JSON.stringify({
accessToken: this.accessToken,
refreshToken: this.refreshToken
}));
} catch (e) {
console.warn('[ApiClient] Failed to save tokens to storage:', e);
}
}
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem('auth_tokens');
}
async request(method, endpoint, data = null) {
// Always reload tokens from storage before making a request
// This handles the case where tokens were set after API client initialization (e.g., auto-login)
this.loadTokensFromStorage();
const url = `${this.baseURL}${endpoint}`;
const options = {
method,
headers: {
'Content-Type': 'application/json'
}
};
if (this.accessToken) {
options.headers['Authorization'] = `Bearer ${this.accessToken}`;
console.log(`[ApiClient] ${method} ${endpoint}: Token present (${this.accessToken.substring(0, 20)}...)`);
} else {
console.warn(`[ApiClient] ${method} ${endpoint}: No token provided`);
}
if (data) {
options.body = JSON.stringify(data);
}
try {
const response = await fetch(url, options);
const result = await response.json();
if (!response.ok) {
// Handle token refresh
if (response.status === 403 && result.code === 'FORBIDDEN' && this.refreshToken) {
await this.refreshAccessToken();
return this.request(method, endpoint, data);
}
throw new Error(result.message || 'API request failed');
}
// Handle both response formats:
// 1. Standard wrapped response: { data: {...} }
// 2. Direct array response: [...]
// 3. Direct object response: {...}
if (Array.isArray(result)) {
return result;
}
return result.data || result;
} catch (error) {
console.error(`[ApiClient] ${method} ${endpoint}:`, error);
throw error;
}
}
async refreshAccessToken() {
try {
const response = await fetch(`${this.baseURL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken })
});
const result = await response.json();
if (result.status === 'success') {
this.accessToken = result.data.tokens.accessToken;
this.refreshToken = result.data.tokens.refreshToken;
this.saveTokensToStorage();
this.dispatchAuthChangeEvent('token-refresh');
return true;
}
this.clearTokens();
this.dispatchAuthChangeEvent('token-expired');
return false;
} catch (error) {
console.error('[ApiClient] Token refresh failed:', error);
this.clearTokens();
this.dispatchAuthChangeEvent('token-refresh-error');
return false;
}
}
/**
* Dispatch auth-change event to notify all listeners of authentication state changes
*/
dispatchAuthChangeEvent(reason = 'auth-change') {
const event = new CustomEvent('auth-change', {
detail: { reason, hasToken: !!this.accessToken },
bubbles: true,
composed: true
});
document.dispatchEvent(event);
window.dispatchEvent(event);
}
// Auth endpoints
async register(email, password, name) {
const result = await this.request('POST', '/auth/register', {
email, password, name
});
this.accessToken = result.tokens.accessToken;
this.refreshToken = result.tokens.refreshToken;
this.saveTokensToStorage();
this.dispatchAuthChangeEvent('user-registered');
return result.user;
}
async login(email, password) {
const result = await this.request('POST', '/auth/login', {
email, password
});
this.accessToken = result.tokens.accessToken;
this.refreshToken = result.tokens.refreshToken;
this.saveTokensToStorage();
this.dispatchAuthChangeEvent('user-logged-in');
return result.user;
}
async getMe() {
const result = await this.request('GET', '/auth/me');
return result.user;
}
async logout() {
this.clearTokens();
this.dispatchAuthChangeEvent('user-logged-out');
}
// Project endpoints
async getProjects() {
const result = await this.request('GET', '/projects');
// Handle both response formats:
// 1. Wrapped response: { data: { projects: [...] } }
// 2. Direct array response: [...]
if (Array.isArray(result)) {
return result;
}
return result.projects || result.data?.projects || [];
}
async getProject(id) {
const result = await this.request('GET', `/projects/${id}`);
return result.project;
}
async createProject(data) {
const result = await this.request('POST', '/projects', data);
return result.project;
}
async updateProject(id, data) {
const result = await this.request('PUT', `/projects/${id}`, data);
return result.project;
}
async deleteProject(id) {
await this.request('DELETE', `/projects/${id}`);
}
// Token endpoints
async getTokens(projectId, category = null) {
const url = `/tokens/project/${projectId}${category ? `?category=${category}` : ''}`;
const result = await this.request('GET', url);
return result.tokens;
}
async createToken(data) {
const result = await this.request('POST', '/tokens', data);
return result.token;
}
async updateToken(id, data) {
const result = await this.request('PUT', `/tokens/${id}`, data);
return result.token;
}
async deleteToken(id) {
await this.request('DELETE', `/tokens/${id}`);
}
// Component endpoints
async getComponents(projectId) {
const result = await this.request('GET', `/components/project/${projectId}`);
return result.components;
}
async createComponent(data) {
const result = await this.request('POST', '/components', data);
return result.component;
}
async updateComponent(id, data) {
const result = await this.request('PUT', `/components/${id}`, data);
return result.component;
}
async deleteComponent(id) {
await this.request('DELETE', `/components/${id}`);
}
}
export default new ApiClient();