/** * Authentication service for handling user login, logout, and token management * * This class provides a complete authentication solution using JWT tokens. * It handles token storage, validation, automatic refresh, and authenticated * API requests. */ class AuthService { /** * Initialises the authentication service * * Loads existing token and user data from localStorage and sets up * automatic token refresh. This ensures the user stays logged in * across page refreshes and that tokens are refreshed before they expire. */ constructor() { this.token = localStorage.getItem('token'); this.refreshToken = localStorage.getItem('refreshToken'); this.user = JSON.parse(localStorage.getItem('user')); this.isValidating = false; this.loginAttempts = parseInt(localStorage.getItem('loginAttempts') || '0'); this.refreshing = false; // Set up token refresh interval this.setupTokenRefresh(); } /** * Sets up automatic token refresh * * Creates an interval that checks token validity every minute and * refreshes it if needed. This helps prevent the user from being * logged out due to token expiration during active use of the application. */ setupTokenRefresh() { // Check token every minute setInterval(() => { this.checkAndRefreshToken(); }, 60000); // 1 minute // Also check immediately this.checkAndRefreshToken(); } /** * Checks if token needs refreshing and refreshes if needed * * This method examines the current token's expiry time and refreshes * it if it's about to expire (within 5 minutes). This provides * seamless authentication */ async checkAndRefreshToken() { // Skip if already refreshing or no token exists if (this.refreshing || !this.token || !this.refreshToken) return; try { this.refreshing = true; // Check if token is about to expire (within 5 minutes) const payload = this.parseJwt(this.token); if (!payload || !payload.exp) return; const expiryTime = payload.exp * 1000; // Convert to milliseconds const currentTime = Date.now(); const timeToExpiry = expiryTime - currentTime; // If token expires in less than 5 minutes, refresh it if (timeToExpiry < 300000) { // 5 minutes in milliseconds await this.refreshAccessToken(); } } catch (error) { console.error('Token refresh check failed:', error); } finally { this.refreshing = false; } } /** * Parses a JWT token to extract its payload * * This utility method decodes the JWT token without verifying * its signature. It's used internally to check token expiry * and extract user information. * * @param {string} token - The JWT token to parse * @returns {Object|null} The decoded token payload or null if invalid */ parseJwt(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 (error) { return null; } } /** * Refreshes the access token using the refresh token * * This method sends the refresh token to the server to obtain * a new access token when the current one is about to expire. * It's a crucial part of maintaining persistent authentication. * * @returns {Promise} True if refresh was successful, false otherwise */ async refreshAccessToken() { if (!this.refreshToken) return false; try { console.log('Attempting to refresh access token...'); const formData = new FormData(); formData.append('action', 'refresh'); formData.append('refreshToken', this.refreshToken); const response = await fetch('/logincontroller.php', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: formData }); console.log('Token refresh response status:', response.status); const data = await response.json(); console.log('Token refresh response data:', data); if (data.valid && data.token) { console.log('Token refresh successful'); this.token = data.token; localStorage.setItem('token', data.token); return true; } // If refresh failed, log out if (!data.valid) { console.log('Token refresh failed, logging out'); this.logout(); } return false; } catch (error) { console.error('Token refresh error details:', error); error_log('Token refresh failed:', error); return false; } } /** * Authenticates a user with the provided credentials * * This method sends the login credentials to the server, * stores the returned tokens and user data if successful, * and updates the login attempts counter for CAPTCHA handling. * * @param {FormData} formData - The form data containing login credentials * @returns {Promise} The login result */ async login(formData) { try { console.log('Attempting to login with URL:', '/logincontroller.php'); const response = await fetch('/logincontroller.php', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: formData }); console.log('Login response status:', response.status); const data = await response.json(); console.log('Login response data:', data); if (data.error) { // If server sends a new CAPTCHA, update it if (data.captcha) { const captchaCode = document.getElementById('captchaCode'); const captchaContainer = document.querySelector('.captcha-container'); if (captchaCode && captchaContainer) { captchaCode.value = data.captcha; captchaContainer.style.display = 'flex'; } } throw new Error(data.error); } if (data.success && data.token) { this.token = data.token; this.refreshToken = data.refreshToken; this.user = data.user; localStorage.setItem('token', data.token); localStorage.setItem('refreshToken', data.refreshToken); localStorage.setItem('user', JSON.stringify(data.user)); // Reset login attempts on successful login localStorage.removeItem('loginAttempts'); // Handle redirect if provided if (data.redirect) { window.location.href = data.redirect; } return data.user; } throw new Error('Login failed'); } catch (error) { console.error('Login error details:', error); error_log('Login error:', error); throw error; } } /** * Logs the user out * * This method clears all authentication data from localStorage * and notifies the server about the logout. It also updates the * UI to reflect the logged-out state. * * @returns {Promise} The logout result */ async logout() { try { const formData = new FormData(); formData.append('action', 'logout'); const response = await fetch('/logincontroller.php', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: formData }); const data = await response.json(); if (!data.success) { throw new Error('Logout failed'); } this.token = null; this.refreshToken = null; this.user = null; localStorage.removeItem('token'); localStorage.removeItem('refreshToken'); localStorage.removeItem('user'); // Handle redirect if provided if (data.redirect) { window.location.href = data.redirect; } } catch (error) { error_log('Logout error:', error); throw error; } } /** * Checks if the user is currently authenticated * * This is a simple helper method that returns true if * the user object exists, indicating an authenticated user. * * @returns {boolean} True if authenticated, false otherwise */ isAuthenticated() { return !!this.token; } /** * Checks if the current user has admin privileges * * This helper method checks if the user is authenticated * and has an access level of 1, which indicates admin status. * * @returns {boolean} True if the user is an admin, false otherwise */ isAdmin() { return this.user && this.user.accessLevel === 1; } /** * Checks if CAPTCHA is required for login * * This method determines if a CAPTCHA should be shown during login * based on the number of failed login attempts. This helps prevent * brute force attacks on the login system. * * @returns {boolean} True if CAPTCHA is required, false otherwise */ needsCaptcha() { return this.loginAttempts >= 3; } /** * Validates the current token with the server * * This method checks if the current token is still valid by * making a request to the server. It's used to verify authentication * status when the application loads. * * @returns {Promise} True if token is valid, false otherwise */ async validateToken() { // Skip validation if no token exists if (!this.token) return false; // Prevent multiple simultaneous validations if (this.isValidating) return true; try { console.log('Validating token...'); this.isValidating = true; const response = await fetch('/auth.php', { headers: { 'Authorization': `Bearer ${this.token}` } }); console.log('Token validation response status:', response.status); const data = await response.json(); console.log('Token validation response data:', data); // Handle invalid token if (!response.ok || !data.valid) { console.log('Token is invalid, attempting to refresh...'); // Try to refresh the token if (this.refreshToken) { console.log('Refresh token exists, attempting to refresh...'); const refreshed = await this.refreshAccessToken(); if (refreshed) { console.log('Token refreshed successfully'); return true; } } console.log('Token refresh failed, logging out'); this.logout(); return false; } console.log('Token is valid'); return true; } catch (error) { console.error('Token validation error details:', error); error_log('Token validation error:', error); return false; } finally { this.isValidating = false; } } /** * Gets the current user object * * This helper method returns the user object containing * information about the authenticated user. * * @returns {Object|null} The user object or null if not authenticated */ getUser() { console.log('Getting user from auth service:', this.user); if (!this.user) { // Try to get user from localStorage as a fallback const localUser = localStorage.getItem('user'); console.log('User not found in auth service, checking localStorage:', localUser); if (localUser) { try { this.user = JSON.parse(localUser); } catch (e) { console.error('Error parsing user from localStorage:', e); } } } return this.user; } /** * Gets the current JWT token * * This helper method returns the current JWT token for * use in authenticated API requests. * * @returns {string|null} The JWT token or null if not authenticated */ getToken() { console.log('Getting token from auth service:', this.token); if (!this.token) { // Try to get token from localStorage as a fallback const localToken = localStorage.getItem('token'); console.log('Token not found in auth service, checking localStorage:', localToken); if (localToken) { this.token = localToken; } } return this.token; } /** * Creates an authenticated fetch function * * This method returns a wrapper around the fetch API that * automatically includes the JWT token in the Authorization header. * It also handles token refresh if a request fails due to an expired token. * * @returns {Function} The authenticated fetch function */ createAuthFetch() { return async (url, options = {}) => { // Ensure options.headers exists options.headers = options.headers || {}; // Add Authorization header if token exists if (this.token) { options.headers['Authorization'] = `Bearer ${this.token}`; } // Add X-Requested-With header for AJAX requests options.headers['X-Requested-With'] = 'XMLHttpRequest'; try { // Make the request const response = await fetch(url, options); // If unauthorized (401), try to refresh the token and retry if (response.status === 401 && this.refreshToken) { console.log('Received 401 response, attempting to refresh token...'); const refreshed = await this.refreshAccessToken(); if (refreshed) { console.log('Token refreshed, retrying request...'); // Update Authorization header with new token options.headers['Authorization'] = `Bearer ${this.token}`; // Retry the request return fetch(url, options); } console.log('Token refresh failed, returning 401 response'); } return response; } catch (error) { console.error('Auth fetch error details:', error); error_log('Auth fetch error:', error); throw error; } }; } /** * Checks if the token is valid by parsing it * * This method checks if the token is valid by parsing it and checking * if it has expired. It doesn't make a server request, so it's only * a client-side check. * * @returns {boolean} True if the token is valid, false otherwise */ isTokenValid() { const token = this.getToken(); if (!token) { console.log('No token found'); return false; } try { const payload = this.parseJwt(token); console.log('Token payload:', payload); if (!payload || !payload.exp) { console.log('Token payload is invalid or missing expiry'); return false; } const now = Math.floor(Date.now() / 1000); const isValid = payload.exp > now; console.log('Token expiry:', new Date(payload.exp * 1000)); console.log('Current time:', new Date(now * 1000)); console.log('Token is valid:', isValid); return isValid; } catch (e) { console.error('Error parsing token:', e); return false; } } } // Initialize authentication service const auth = new AuthService(); // Create authenticated fetch function const authFetch = auth.createAuthFetch(); // Set up authentication handlers when DOM is loaded document.addEventListener('DOMContentLoaded', () => { console.log('DOM loaded, setting up authentication handlers'); // Get modal elements const loginButton = document.querySelector('[data-bs-toggle="modal"]'); const loginModal = document.getElementById('loginModal'); // Set up login form handlers const loginForm = document.querySelector('#loginModal form'); const loginError = document.querySelector('#loginError'); const captchaContainer = document.querySelector('.captcha-container'); if (loginForm) { // Handle form submission loginForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(loginForm); formData.append('action', 'login'); try { await auth.login(formData); // Hide modal before reload const modal = bootstrap.Modal.getInstance(loginModal); if (modal) { modal.hide(); } // Refresh the page using replace to prevent form resubmission window.location.replace(window.location.href); } catch (error) { if (loginError) { loginError.textContent = error.message; loginError.style.display = 'block'; } // Show CAPTCHA after 3 failed attempts if (auth.needsCaptcha() && captchaContainer) { captchaContainer.style.display = 'flex'; } } }); } // Set up logout button handler const logoutButton = document.getElementById('logoutButton'); if (logoutButton) { logoutButton.addEventListener('click', async (e) => { e.preventDefault(); try { await auth.logout(); window.location.reload(); } catch (error) { console.error('Logout failed:', error); } }); } // Validate token if authenticated if (auth.isAuthenticated()) { console.log('User is authenticated, validating token...'); // Check if we're already in a validation cycle const validationCycle = sessionStorage.getItem('validationCycle'); if (validationCycle) { console.log('Already in validation cycle, forcing logout'); // We've already tried to validate in this page load // Force a proper logout sessionStorage.removeItem('validationCycle'); auth.logout().finally(() => { window.location.replace('/'); }); return; } // Set validation cycle flag sessionStorage.setItem('validationCycle', 'true'); auth.validateToken().then(valid => { console.log('Token validation result:', valid); if (!valid) { console.log('Token is invalid, redirecting to login page'); // Token is invalid, redirect to login page window.location.replace('/'); } else { console.log('Token is valid, clearing validation cycle flag'); // Clear validation cycle flag sessionStorage.removeItem('validationCycle'); } }); } else { console.log('User is not authenticated'); } }); // Export auth service and authenticated fetch window.auth = auth; window.authFetch = authFetch; // Make parseJwt accessible from outside window.auth.parseJwt = auth.parseJwt.bind(auth); /** * Custom error logging function * * This utility function provides a consistent way to log errors * throughout the application. It includes both a message and the * error object for better debugging. * * @param {string} message - The error message * @param {Error} error - The error object */ function error_log(message, error) { console.error(message, error); }