UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

304 lines 10.5 kB
export class AuthService { constructor() { this.listeners = new Set(); this.refreshTimer = null; this.authState = this.getInitialState(); this.setupTokenRefresh(); } static getInstance() { if (!AuthService.instance) { AuthService.instance = new AuthService(); } return AuthService.instance; } getInitialState() { try { const token = localStorage.getItem(AuthService.TOKEN_KEY); const refreshToken = localStorage.getItem(AuthService.REFRESH_TOKEN_KEY); const userStr = localStorage.getItem(AuthService.USER_KEY); if (token && userStr) { if (this.isTokenExpired(token)) { this.clearStoredAuth(); return this.getUnauthenticatedState(); } const user = JSON.parse(userStr); return { isAuthenticated: true, user, token, refreshToken, isLoading: false, error: null, }; } } catch (error) { console.warn('Failed to load auth state from localStorage:', error); this.clearStoredAuth(); } return this.getUnauthenticatedState(); } getUnauthenticatedState() { return { isAuthenticated: false, user: null, token: null, refreshToken: null, isLoading: false, error: null, }; } getAuthState() { return { ...this.authState }; } subscribe(callback) { this.listeners.add(callback); return () => this.listeners.delete(callback); } setState(updates) { this.authState = { ...this.authState, ...updates }; this.notifyListeners(); } notifyListeners() { this.listeners.forEach(callback => { try { callback(this.getAuthState()); } catch (error) { console.error('Error in auth state listener:', error); } }); } async login(credentials) { this.setState({ isLoading: true, error: null }); try { const response = await fetch('/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(credentials), credentials: 'same-origin', }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || `Authentication failed: ${response.status}`); } const data = await response.json(); if (!data.success || !data.token || !data.user) { throw new Error('Invalid response from authentication server'); } this.storeAuthData(data); this.setState({ isAuthenticated: true, user: data.user, token: data.token, refreshToken: data.refreshToken, isLoading: false, error: null, }); this.setupTokenRefresh(data.expiresIn); console.log('✅ User authenticated successfully:', data.user.username); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Authentication failed'; console.error('❌ Authentication failed:', errorMessage); this.setState({ isLoading: false, error: errorMessage, }); return false; } } async logout() { try { if (this.authState.token) { await fetch('/auth/logout', { method: 'POST', headers: { Authorization: `Bearer ${this.authState.token}`, 'Content-Type': 'application/json', }, credentials: 'same-origin', }).catch(error => { console.warn('Failed to notify backend of logout:', error); }); } } finally { this.clearAuth(); console.log('✅ User logged out successfully'); } } async refreshToken() { if (!this.authState.refreshToken) { console.warn('No refresh token available'); return false; } try { const response = await fetch('/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ refreshToken: this.authState.refreshToken, }), credentials: 'same-origin', }); if (!response.ok) { throw new Error(`Token refresh failed: ${response.status}`); } const data = await response.json(); if (!data.success || !data.token) { throw new Error('Invalid refresh response'); } localStorage.setItem(AuthService.TOKEN_KEY, data.token); this.setState({ token: data.token }); this.setupTokenRefresh(data.expiresIn); console.log('✅ Token refreshed successfully'); return true; } catch (error) { console.error('❌ Token refresh failed:', error); this.clearAuth(); return false; } } async getCurrentUser() { if (!this.authState.token) { return null; } try { const response = await fetch('/auth/me', { headers: { Authorization: `Bearer ${this.authState.token}`, }, credentials: 'same-origin', }); if (!response.ok) { if (response.status === 401) { const refreshed = await this.refreshToken(); if (!refreshed) { return null; } return this.getCurrentUser(); } throw new Error(`Failed to get user: ${response.status}`); } const data = await response.json(); const user = data.user || data; localStorage.setItem(AuthService.USER_KEY, JSON.stringify(user)); this.setState({ user }); return user; } catch (error) { console.error('Failed to get current user:', error); return null; } } async checkAuthHealth() { try { const response = await fetch('/auth/health'); return response.ok; } catch (error) { console.error('Auth health check failed:', error); return false; } } storeAuthData(data) { try { localStorage.setItem(AuthService.TOKEN_KEY, data.token); localStorage.setItem(AuthService.USER_KEY, JSON.stringify(data.user)); if (data.refreshToken) { localStorage.setItem(AuthService.REFRESH_TOKEN_KEY, data.refreshToken); } } catch (error) { console.error('Failed to store auth data:', error); throw new Error('Failed to save authentication data'); } } clearAuth() { this.clearStoredAuth(); if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = null; } this.setState(this.getUnauthenticatedState()); } clearStoredAuth() { localStorage.removeItem(AuthService.TOKEN_KEY); localStorage.removeItem(AuthService.REFRESH_TOKEN_KEY); localStorage.removeItem(AuthService.USER_KEY); } isTokenExpired(token) { try { const payload = JSON.parse(atob(token.split('.')[1])); const now = Math.floor(Date.now() / 1000); const bufferTime = 60; return payload.exp < now + bufferTime; } catch (error) { console.warn('Failed to decode token:', error); return true; } } setupTokenRefresh(expiresIn) { if (this.refreshTimer) { clearTimeout(this.refreshTimer); } if (!this.authState.refreshToken) { return; } const expires = expiresIn || 15 * 60; const refreshTime = Math.min(expires - 120, expires * 0.8) * 1000; if (refreshTime > 0) { this.refreshTimer = window.setTimeout(() => { this.refreshToken().catch(error => { console.error('Automatic token refresh failed:', error); }); }, refreshTime); } } getAuthHeader() { return this.authState.token ? `Bearer ${this.authState.token}` : null; } async authenticatedFetch(url, options = {}) { const authHeader = this.getAuthHeader(); if (!authHeader) { throw new Error('Authentication required'); } const headers = { ...options.headers, Authorization: authHeader, 'Content-Type': 'application/json', }; const response = await fetch(url, { ...options, headers, credentials: 'same-origin', }); if (response.status === 401 && this.authState.refreshToken) { const refreshed = await this.refreshToken(); if (refreshed) { const newAuthHeader = this.getAuthHeader(); if (newAuthHeader) { headers.Authorization = newAuthHeader; return fetch(url, { ...options, headers, credentials: 'same-origin' }); } } this.clearAuth(); throw new Error('Authentication expired'); } return response; } } AuthService.TOKEN_KEY = 'quiz-auth-token'; AuthService.REFRESH_TOKEN_KEY = 'quiz-refresh-token'; AuthService.USER_KEY = 'quiz-user'; const authService = AuthService.getInstance(); export default authService; //# sourceMappingURL=AuthService.js.map