fitpub/src/main/resources/static/js/auth.js
2026-01-01 23:48:05 +01:00

417 lines
12 KiB
JavaScript

// FitPub - Authentication Management
/**
* Authentication utilities for managing JWT tokens and user sessions
*/
const FitPubAuth = {
/**
* Get the stored JWT token
* @returns {string|null} JWT token or null if not found
*/
getToken: function() {
return localStorage.getItem('jwtToken');
},
/**
* Store JWT token
* @param {string} token - JWT token to store
*/
setToken: function(token) {
localStorage.setItem('jwtToken', token);
},
/**
* Remove stored JWT token
*/
removeToken: function() {
localStorage.removeItem('jwtToken');
localStorage.removeItem('username');
},
/**
* Get the stored username
* @returns {string|null} Username or null if not found
*/
getUsername: function() {
return localStorage.getItem('username');
},
/**
* Store username
* @param {string} username - Username to store
*/
setUsername: function(username) {
localStorage.setItem('username', username);
},
/**
* Check if user is authenticated
* @returns {boolean} True if authenticated, false otherwise
*/
isAuthenticated: function() {
const token = this.getToken();
if (!token) {
return false;
}
// Check if token is expired
try {
const payload = this.parseJwt(token);
const now = Math.floor(Date.now() / 1000);
if (payload.exp && payload.exp < now) {
// Token expired, remove it
this.removeToken();
return false;
}
return true;
} catch (e) {
console.error('Error parsing JWT:', e);
this.removeToken();
return false;
}
},
/**
* Parse JWT token to extract payload
* @param {string} token - JWT token
* @returns {object} Decoded payload
*/
parseJwt: function(token) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join('')
);
return JSON.parse(jsonPayload);
} catch (e) {
console.error('Error parsing JWT:', e);
throw e;
}
},
/**
* Get time until token expiration
* @returns {number} Seconds until expiration, or 0 if expired/invalid
*/
getTokenExpirationTime: function() {
const token = this.getToken();
if (!token) {
return 0;
}
try {
const payload = this.parseJwt(token);
const now = Math.floor(Date.now() / 1000);
if (payload.exp) {
return Math.max(0, payload.exp - now);
}
return 0;
} catch (e) {
return 0;
}
},
/**
* Logout user
*/
logout: function() {
this.removeToken();
window.location.href = '/login';
},
/**
* Make an authenticated API request
* @param {string} url - API endpoint URL
* @param {object} options - Fetch options
* @returns {Promise<Response>} Fetch response
*/
authenticatedFetch: async function(url, options = {}) {
const token = this.getToken();
if (!token) {
throw new Error('No authentication token found');
}
// Add Authorization header
const headers = {
...options.headers,
'Authorization': `Bearer ${token}`,
};
// If body is an object, set Content-Type to JSON
if (options.body && typeof options.body === 'object') {
headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(options.body);
}
const response = await fetch(url, {
...options,
headers
});
// If unauthorized, redirect to login
if (response.status === 401) {
this.removeToken();
window.location.href = '/login';
throw new Error('Authentication failed');
}
return response;
},
/**
* Initialize authentication checks and setup
*/
init: function() {
// Update navigation UI based on auth status
this.updateNavigationUI();
// Check registration status and update UI
this.checkRegistrationStatus();
// Check authentication status on page load
this.checkAuthStatus();
// Set up session expiration warning
this.setupExpirationWarning();
},
/**
* Check if registration is enabled and update navigation UI
*/
checkRegistrationStatus: async function() {
try {
const response = await fetch('/api/auth/registration-status');
const data = await response.json();
if (!data.enabled) {
// Hide registration link in navigation
const registerLinks = document.querySelectorAll('a[href="/register"]');
registerLinks.forEach(link => {
const parent = link.parentElement;
if (parent && parent.tagName === 'LI') {
parent.style.display = 'none';
}
});
}
} catch (error) {
console.error('Error checking registration status:', error);
// Continue without hiding registration link if check fails
}
},
/**
* Update navigation UI based on authentication status
*/
updateNavigationUI: function() {
const authUserMenu = document.getElementById('authUserMenu');
const guestMenu = document.getElementById('guestMenu');
const usernameDisplay = document.getElementById('usernameDisplay');
const myActivitiesLink = document.getElementById('myActivitiesLink');
const uploadLink = document.getElementById('uploadLink');
const analyticsLink = document.getElementById('analyticsLink');
const heatmapLink = document.getElementById('heatmapLink');
const notificationsBell = document.getElementById('notificationsBell');
if (this.isAuthenticated()) {
// Show authenticated menu, hide guest menu
if (authUserMenu) {
authUserMenu.classList.remove('d-none');
}
if (guestMenu) {
guestMenu.style.display = 'none';
}
// Show authenticated navigation links
if (myActivitiesLink) {
myActivitiesLink.style.display = '';
myActivitiesLink.parentElement.style.display = '';
}
if (uploadLink) {
uploadLink.style.display = '';
uploadLink.parentElement.style.display = '';
}
if (analyticsLink) {
analyticsLink.style.display = '';
analyticsLink.parentElement.style.display = '';
}
if (heatmapLink) {
heatmapLink.style.display = '';
heatmapLink.parentElement.style.display = '';
}
// Show notifications bell
if (notificationsBell) {
notificationsBell.classList.remove('d-none');
}
// Display username
const username = this.getUsername();
if (usernameDisplay && username) {
usernameDisplay.textContent = username;
}
// Start polling for unread notifications
this.startNotificationPolling();
} else {
// Show guest menu, hide authenticated menu
if (authUserMenu) {
authUserMenu.classList.add('d-none');
}
if (notificationsBell) {
notificationsBell.classList.add('d-none');
}
if (guestMenu) {
guestMenu.style.display = '';
}
// Hide authenticated navigation links
if (myActivitiesLink) {
myActivitiesLink.style.display = 'none';
myActivitiesLink.parentElement.style.display = 'none';
}
if (uploadLink) {
uploadLink.style.display = 'none';
uploadLink.parentElement.style.display = 'none';
}
if (analyticsLink) {
analyticsLink.style.display = 'none';
analyticsLink.parentElement.style.display = 'none';
}
if (heatmapLink) {
heatmapLink.style.display = 'none';
heatmapLink.parentElement.style.display = 'none';
}
}
},
/**
* Check authentication status and handle accordingly
*/
checkAuthStatus: function() {
const currentPath = window.location.pathname;
const publicPaths = ['/', '/login', '/register', '/timeline'];
// Skip check for public paths
if (publicPaths.includes(currentPath)) {
return;
}
// Activity detail pages are public (for viewing public activities)
// Pattern: /activities/{uuid}
if (currentPath.startsWith('/activities/') && currentPath.split('/').length === 3) {
return;
}
// Check if authenticated
if (!this.isAuthenticated()) {
// Redirect to login for protected pages
window.location.href = '/login?redirect=' + encodeURIComponent(currentPath);
}
},
/**
* Set up warning for session expiration
*/
setupExpirationWarning: function() {
const token = this.getToken();
if (!token) {
return;
}
const expirationTime = this.getTokenExpirationTime();
if (expirationTime > 0) {
// Warn 5 minutes before expiration
const warningTime = Math.max(0, (expirationTime - 300) * 1000);
setTimeout(() => {
if (this.isAuthenticated()) {
this.showExpirationWarning();
}
}, warningTime);
}
},
/**
* Show session expiration warning
*/
showExpirationWarning: function() {
if (window.FitPub && window.FitPub.showAlert) {
window.FitPub.showAlert(
'Your session will expire soon. Please save your work.',
'warning'
);
} else {
console.warn('Session expiring soon');
}
},
/**
* Refresh the current page with authentication
*/
refreshPage: function() {
window.location.reload();
},
/**
* Start polling for unread notifications
*/
notificationPollInterval: null,
startNotificationPolling: function() {
// Stop any existing polling
this.stopNotificationPolling();
// Initial fetch
this.updateUnreadNotificationCount();
// Poll every 30 seconds
this.notificationPollInterval = setInterval(() => {
this.updateUnreadNotificationCount();
}, 30000);
},
stopNotificationPolling: function() {
if (this.notificationPollInterval) {
clearInterval(this.notificationPollInterval);
this.notificationPollInterval = null;
}
},
async updateUnreadNotificationCount() {
try {
const response = await this.authenticatedFetch('/api/notifications/unread/count');
if (response.ok) {
const data = await response.json();
const badge = document.getElementById('navNotificationCount');
if (badge) {
if (data.count > 0) {
badge.textContent = data.count > 99 ? '99+' : data.count;
badge.style.display = 'inline-block';
} else {
badge.style.display = 'none';
}
}
}
} catch (error) {
// Silently fail - don't spam console with errors
console.debug('Failed to fetch notification count:', error);
}
}
};
// Initialize authentication on page load
document.addEventListener('DOMContentLoaded', function() {
FitPubAuth.init();
});
// Make available globally
window.FitPubAuth = FitPubAuth;