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.
194 lines • 6.99 kB
JavaScript
import { AuthService } from '../services/AuthService';
export class SSEAuthHandler {
constructor(options) {
this.reconnectTimer = null;
this.unsubscribeAuth = null;
this.authService = AuthService.getInstance();
this.options = {
withCredentials: true,
reconnectInterval: 5000,
maxReconnectAttempts: 5,
...options,
};
this.connection = {
eventSource: null,
isConnected: false,
reconnectAttempts: 0,
url: options.url,
};
this.setupAuthSubscription();
}
async connect() {
const authState = this.authService.getAuthState();
if (!authState.isAuthenticated || !authState.token) {
console.warn('SSE connection requires authentication');
this.options.onAuthRequired?.();
return false;
}
try {
await this.establishConnection();
return true;
}
catch (error) {
console.error('Failed to establish SSE connection:', error);
this.options.onError?.(error);
return false;
}
}
disconnect() {
this.clearReconnectTimer();
if (this.connection.eventSource) {
this.connection.eventSource.close();
this.connection.eventSource = null;
}
this.connection.isConnected = false;
this.connection.reconnectAttempts = 0;
console.log('SSE connection disconnected');
}
isConnected() {
return (this.connection.isConnected && this.connection.eventSource?.readyState === EventSource.OPEN);
}
getConnectionStatus() {
return { ...this.connection };
}
async establishConnection() {
const authHeader = this.authService.getAuthHeader();
if (!authHeader) {
throw new Error('No authentication token available');
}
const token = authHeader.replace('Bearer ', '');
const separator = this.options.url.includes('?') ? '&' : '?';
const authenticatedUrl = `${this.options.url}${separator}token=${encodeURIComponent(token)}`;
this.connection.eventSource = new EventSource(authenticatedUrl, {
withCredentials: this.options.withCredentials,
});
this.setupEventListeners();
}
setupEventListeners() {
if (!this.connection.eventSource)
return;
this.connection.eventSource.onopen = event => {
console.log('SSE connection established');
this.connection.isConnected = true;
this.connection.reconnectAttempts = 0;
this.clearReconnectTimer();
this.options.onOpen?.(event);
};
this.connection.eventSource.onmessage = event => {
this.options.onMessage?.(event);
};
this.connection.eventSource.onerror = event => {
console.error('SSE connection error:', event);
this.connection.isConnected = false;
if (this.connection.eventSource?.readyState === EventSource.CLOSED) {
this.handleConnectionError();
}
this.options.onError?.(event);
};
this.setupCustomEventHandlers();
}
setupCustomEventHandlers() {
if (!this.connection.eventSource)
return;
this.connection.eventSource.addEventListener('auth-error', event => {
console.warn('SSE authentication error:', event);
this.handleAuthError();
});
this.connection.eventSource.addEventListener('token-refresh', event => {
console.log('Token refresh requested by server');
this.handleTokenRefresh();
});
this.connection.eventSource.addEventListener('close', event => {
console.log('Server requested connection close');
this.disconnect();
this.options.onClose?.(event);
});
}
handleConnectionError() {
if (this.connection.reconnectAttempts >= (this.options.maxReconnectAttempts || 5)) {
console.error('Max reconnection attempts reached');
this.options.onAuthRequired?.();
return;
}
this.connection.reconnectAttempts++;
const delay = this.options.reconnectInterval * Math.pow(2, this.connection.reconnectAttempts - 1);
console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.connection.reconnectAttempts})`);
this.reconnectTimer = window.setTimeout(() => {
this.reconnect();
}, delay);
}
handleAuthError() {
console.warn('SSE authentication failed, requesting login');
this.disconnect();
this.options.onAuthRequired?.();
}
async handleTokenRefresh() {
try {
const refreshed = await this.authService.refreshToken();
if (refreshed) {
console.log('Token refreshed, reconnecting SSE');
await this.reconnect();
}
else {
console.error('Token refresh failed');
this.handleAuthError();
}
}
catch (error) {
console.error('Token refresh error:', error);
this.handleAuthError();
}
}
async reconnect() {
console.log('Attempting SSE reconnection');
if (this.connection.eventSource) {
this.connection.eventSource.close();
}
try {
await this.establishConnection();
return true;
}
catch (error) {
console.error('SSE reconnection failed:', error);
this.handleConnectionError();
return false;
}
}
setupAuthSubscription() {
this.unsubscribeAuth = this.authService.subscribe(authState => {
if (!authState.isAuthenticated && this.connection.isConnected) {
console.log('User logged out, disconnecting SSE');
this.disconnect();
}
});
}
clearReconnectTimer() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
addEventListener(type, listener) {
this.connection.eventSource?.addEventListener(type, listener);
}
removeEventListener(type, listener) {
this.connection.eventSource?.removeEventListener(type, listener);
}
destroy() {
this.disconnect();
this.unsubscribeAuth?.();
this.clearReconnectTimer();
}
}
export function createAuthenticatedSSE(options) {
return new SSEAuthHandler(options);
}
export const SSEEventTypes = {
QUIZ_UPDATE: 'quiz-update',
USER_PROGRESS: 'user-progress',
SYSTEM_NOTIFICATION: 'system-notification',
AUTH_ERROR: 'auth-error',
TOKEN_REFRESH: 'token-refresh',
CONNECTION_CLOSE: 'close',
};
//# sourceMappingURL=SSEAuthHandler.js.map