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
414 lines
10 KiB
JavaScript
414 lines
10 KiB
JavaScript
/**
|
|
* user-store.js
|
|
* User state management store
|
|
* Manages current logged-in user, preferences, and integrations
|
|
* MVP3: Integrates with backend API while maintaining localStorage persistence
|
|
*/
|
|
|
|
import apiClient from '../services/api-client.js';
|
|
|
|
export class UserStore {
|
|
constructor() {
|
|
// Current user state
|
|
this.currentUser = this.loadUserFromStorage();
|
|
this.isAuthenticated = !!this.currentUser;
|
|
this.isLoading = false;
|
|
|
|
// User preferences
|
|
this.preferences = this.loadPreferencesFromStorage() || this.getDefaultPreferences();
|
|
|
|
// User integrations (API keys, tokens)
|
|
this.integrations = this.loadIntegrationsFromStorage() || {};
|
|
|
|
// Event listeners
|
|
this.listeners = new Set();
|
|
|
|
// Try to verify current user with API on initialization
|
|
if (this.isAuthenticated) {
|
|
this.verifyUserWithAPI();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get default user preferences
|
|
* @returns {Object} Default preferences
|
|
*/
|
|
getDefaultPreferences() {
|
|
return {
|
|
theme: 'dark',
|
|
language: 'en',
|
|
lastTeam: 'ui',
|
|
chatCollapsedState: true,
|
|
notifications: {
|
|
enabled: true,
|
|
email: true,
|
|
desktop: true
|
|
},
|
|
layout: {
|
|
showChat: true,
|
|
showPanel: true,
|
|
sidebarWidth: 250
|
|
},
|
|
editor: {
|
|
fontSize: 13,
|
|
fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
lineHeight: 1.6
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Load user from localStorage
|
|
* @returns {Object|null} User object or null
|
|
*/
|
|
loadUserFromStorage() {
|
|
try {
|
|
const stored = localStorage.getItem('current_user');
|
|
return stored ? JSON.parse(stored) : null;
|
|
} catch (error) {
|
|
console.warn('[UserStore] Failed to parse stored user:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load preferences from localStorage
|
|
* @returns {Object|null} Preferences or null
|
|
*/
|
|
loadPreferencesFromStorage() {
|
|
try {
|
|
const stored = localStorage.getItem('user_preferences');
|
|
return stored ? JSON.parse(stored) : null;
|
|
} catch (error) {
|
|
console.warn('[UserStore] Failed to parse stored preferences:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load integrations from localStorage
|
|
* @returns {Object} Integrations object
|
|
*/
|
|
loadIntegrationsFromStorage() {
|
|
try {
|
|
const stored = localStorage.getItem('user_integrations');
|
|
return stored ? JSON.parse(stored) : {};
|
|
} catch (error) {
|
|
console.warn('[UserStore] Failed to parse stored integrations:', error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Persist user to localStorage
|
|
*/
|
|
persistUser() {
|
|
if (this.currentUser) {
|
|
localStorage.setItem('current_user', JSON.stringify(this.currentUser));
|
|
} else {
|
|
localStorage.removeItem('current_user');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Persist preferences to localStorage
|
|
*/
|
|
persistPreferences() {
|
|
localStorage.setItem('user_preferences', JSON.stringify(this.preferences));
|
|
}
|
|
|
|
/**
|
|
* Persist integrations to localStorage
|
|
*/
|
|
persistIntegrations() {
|
|
localStorage.setItem('user_integrations', JSON.stringify(this.integrations));
|
|
}
|
|
|
|
/**
|
|
* Verify current user with API
|
|
*/
|
|
async verifyUserWithAPI() {
|
|
try {
|
|
const user = await apiClient.getMe();
|
|
this.currentUser = user;
|
|
this.isAuthenticated = true;
|
|
this.persistUser();
|
|
this.notifyListeners();
|
|
} catch (error) {
|
|
console.warn('[UserStore] Failed to verify user with API:', error);
|
|
// Fall back to localStorage user
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register new user
|
|
* @param {string} email - User email
|
|
* @param {string} password - User password
|
|
* @param {string} name - User name
|
|
* @returns {Object} Created user
|
|
*/
|
|
async register(email, password, name) {
|
|
this.isLoading = true;
|
|
this.notifyListeners();
|
|
|
|
try {
|
|
const user = await apiClient.register(email, password, name);
|
|
this.currentUser = user;
|
|
this.isAuthenticated = true;
|
|
this.persistUser();
|
|
this.notifyListeners();
|
|
return user;
|
|
} catch (error) {
|
|
this.isLoading = false;
|
|
this.notifyListeners();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Login user
|
|
* @param {string} email - User email
|
|
* @param {string} password - User password
|
|
* @returns {Object} Logged-in user
|
|
*/
|
|
async login(email, password) {
|
|
this.isLoading = true;
|
|
this.notifyListeners();
|
|
|
|
try {
|
|
const user = await apiClient.login(email, password);
|
|
this.currentUser = user;
|
|
this.isAuthenticated = true;
|
|
this.persistUser();
|
|
this.isLoading = false;
|
|
this.notifyListeners();
|
|
return user;
|
|
} catch (error) {
|
|
this.isLoading = false;
|
|
this.isAuthenticated = false;
|
|
this.notifyListeners();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logout user
|
|
*/
|
|
async logout() {
|
|
try {
|
|
await apiClient.logout();
|
|
} catch (error) {
|
|
console.warn('[UserStore] API logout failed:', error);
|
|
}
|
|
|
|
this.currentUser = null;
|
|
this.isAuthenticated = false;
|
|
this.integrations = {};
|
|
|
|
localStorage.removeItem('current_user');
|
|
localStorage.removeItem('user_preferences');
|
|
localStorage.removeItem('user_integrations');
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
|
|
this.notifyListeners();
|
|
}
|
|
|
|
/**
|
|
* Get current user
|
|
* @returns {Object|null} Current user or null
|
|
*/
|
|
getCurrentUser() {
|
|
return this.currentUser;
|
|
}
|
|
|
|
/**
|
|
* Check if user is authenticated
|
|
* @returns {boolean} Authentication status
|
|
*/
|
|
isLoggedIn() {
|
|
return this.isAuthenticated;
|
|
}
|
|
|
|
/**
|
|
* Update user profile
|
|
* @param {Object} updates - {name, email, avatar, bio}
|
|
* @returns {Object} Updated user
|
|
*/
|
|
async updateProfile(updates) {
|
|
try {
|
|
// Try API first if authenticated
|
|
if (this.isAuthenticated && apiClient.accessToken) {
|
|
const updated = await apiClient.updateUser(this.currentUser.id, updates);
|
|
this.currentUser = { ...this.currentUser, ...updated };
|
|
this.persistUser();
|
|
this.notifyListeners();
|
|
return updated;
|
|
}
|
|
} catch (error) {
|
|
console.warn('[UserStore] API profile update failed:', error);
|
|
}
|
|
|
|
// Fallback to local update
|
|
this.currentUser = { ...this.currentUser, ...updates };
|
|
this.persistUser();
|
|
this.notifyListeners();
|
|
return this.currentUser;
|
|
}
|
|
|
|
/**
|
|
* Update user preferences
|
|
* @param {Object} updates - Partial preference updates
|
|
* @returns {Object} Updated preferences
|
|
*/
|
|
updatePreferences(updates) {
|
|
this.preferences = { ...this.preferences, ...updates };
|
|
|
|
// Deep merge nested objects
|
|
if (updates.notifications) {
|
|
this.preferences.notifications = { ...this.preferences.notifications, ...updates.notifications };
|
|
}
|
|
if (updates.layout) {
|
|
this.preferences.layout = { ...this.preferences.layout, ...updates.layout };
|
|
}
|
|
if (updates.editor) {
|
|
this.preferences.editor = { ...this.preferences.editor, ...updates.editor };
|
|
}
|
|
|
|
this.persistPreferences();
|
|
this.notifyListeners();
|
|
return this.preferences;
|
|
}
|
|
|
|
/**
|
|
* Get user preferences
|
|
* @returns {Object} Current preferences
|
|
*/
|
|
getPreferences() {
|
|
return { ...this.preferences };
|
|
}
|
|
|
|
/**
|
|
* Add or update integration
|
|
* @param {string} service - Service name (figma, jira, github, slack, storybook)
|
|
* @param {string} apiKey - API key or token for the service
|
|
* @param {Object} metadata - Additional metadata (e.g., {projectKey: 'ABC'})
|
|
* @returns {Object} Updated integrations
|
|
*/
|
|
setIntegration(service, apiKey, metadata = {}) {
|
|
this.integrations[service] = {
|
|
enabled: !!apiKey,
|
|
apiKey,
|
|
...metadata,
|
|
lastUpdated: new Date().toISOString()
|
|
};
|
|
this.persistIntegrations();
|
|
this.notifyListeners();
|
|
return this.integrations;
|
|
}
|
|
|
|
/**
|
|
* Get specific integration
|
|
* @param {string} service - Service name
|
|
* @returns {Object|null} Integration config or null
|
|
*/
|
|
getIntegration(service) {
|
|
return this.integrations[service] || null;
|
|
}
|
|
|
|
/**
|
|
* Get all integrations
|
|
* @returns {Object} All integrations
|
|
*/
|
|
getIntegrations() {
|
|
return { ...this.integrations };
|
|
}
|
|
|
|
/**
|
|
* Remove integration
|
|
* @param {string} service - Service name
|
|
* @returns {Object} Updated integrations
|
|
*/
|
|
removeIntegration(service) {
|
|
delete this.integrations[service];
|
|
this.persistIntegrations();
|
|
this.notifyListeners();
|
|
return this.integrations;
|
|
}
|
|
|
|
/**
|
|
* Get user avatar (with fallback)
|
|
* @returns {string} Avatar URL or initials
|
|
*/
|
|
getAvatar() {
|
|
if (!this.currentUser) return null;
|
|
if (this.currentUser.avatar) return this.currentUser.avatar;
|
|
|
|
// Generate initials-based avatar
|
|
const initials = (this.currentUser.name || this.currentUser.email)
|
|
.split(' ')
|
|
.map(n => n[0])
|
|
.join('')
|
|
.toUpperCase();
|
|
|
|
return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect fill='%23007acc' width='32' height='32'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='white' font-size='14' font-weight='bold' font-family='sans-serif' transform='translate(0, 1)'%3E${initials}%3C/text%3E%3C/svg%3E`;
|
|
}
|
|
|
|
/**
|
|
* Get user display name
|
|
* @returns {string} Name or email
|
|
*/
|
|
getDisplayName() {
|
|
if (!this.currentUser) return 'Anonymous';
|
|
return this.currentUser.name || this.currentUser.email;
|
|
}
|
|
|
|
/**
|
|
* Subscribe to user state changes
|
|
* @param {Function} callback - Called with {currentUser, preferences, integrations, isAuthenticated}
|
|
* @returns {Function} Unsubscribe function
|
|
*/
|
|
subscribe(callback) {
|
|
this.listeners.add(callback);
|
|
return () => this.listeners.delete(callback);
|
|
}
|
|
|
|
/**
|
|
* Notify all listeners of state changes
|
|
*/
|
|
notifyListeners() {
|
|
this.listeners.forEach(listener => listener({
|
|
currentUser: this.currentUser,
|
|
preferences: this.getPreferences(),
|
|
integrations: this.getIntegrations(),
|
|
isAuthenticated: this.isAuthenticated
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Reset to default state
|
|
*/
|
|
reset() {
|
|
this.currentUser = null;
|
|
this.isAuthenticated = false;
|
|
this.preferences = this.getDefaultPreferences();
|
|
this.integrations = {};
|
|
|
|
localStorage.removeItem('current_user');
|
|
localStorage.removeItem('user_preferences');
|
|
localStorage.removeItem('user_integrations');
|
|
|
|
this.notifyListeners();
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let userStoreInstance = null;
|
|
|
|
export function useUserStore() {
|
|
if (!userStoreInstance) {
|
|
userStoreInstance = new UserStore();
|
|
}
|
|
return userStoreInstance;
|
|
}
|