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
JavaScript
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