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
This commit is contained in:
268
admin-ui/js/services/api-client.js
Normal file
268
admin-ui/js/services/api-client.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user