Files
michaelschiemer/resources/js/modules/security/CsrfManager.js
2025-11-24 21:28:25 +01:00

222 lines
5.8 KiB
JavaScript

/**
* CSRF Manager
*
* Handles CSRF token management including automatic refresh.
*/
import { Logger } from '../../core/logger.js';
/**
* CsrfManager - CSRF token management
*/
export class CsrfManager {
constructor(config = {}) {
this.config = {
tokenName: config.tokenName || '_token',
headerName: config.headerName || 'X-CSRF-TOKEN',
refreshInterval: config.refreshInterval || 30 * 60 * 1000, // 30 minutes
autoRefresh: config.autoRefresh ?? true,
endpoint: config.endpoint || '/api/csrf/token',
...config
};
this.currentToken = null;
this.refreshTimer = null;
this.isRefreshing = false;
// Initialize
this.init();
}
/**
* Create a new CsrfManager instance
*/
static create(config = {}) {
return new CsrfManager(config);
}
/**
* Initialize CSRF manager
*/
init() {
// Get initial token from meta tag or form
this.currentToken = this.getTokenFromPage();
if (!this.currentToken) {
Logger.warn('[CsrfManager] No CSRF token found on page');
} else {
Logger.info('[CsrfManager] Initialized with token');
}
// Set up auto-refresh if enabled
if (this.config.autoRefresh) {
this.startAutoRefresh();
}
// Update all forms and meta tags
this.updateAllTokens();
}
/**
* Get token from page (meta tag or form)
*/
getTokenFromPage() {
// Try meta tag first
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
return metaTag.getAttribute('content');
}
// Try form input
const formInput = document.querySelector(`input[name="${this.config.tokenName}"]`);
if (formInput) {
return formInput.value;
}
return null;
}
/**
* Get current CSRF token
*/
getToken() {
return this.currentToken;
}
/**
* Refresh CSRF token
*/
async refreshToken() {
if (this.isRefreshing) {
Logger.debug('[CsrfManager] Token refresh already in progress');
return;
}
this.isRefreshing = true;
try {
const response = await fetch(this.config.endpoint, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Failed to refresh token: ${response.status}`);
}
const data = await response.json();
const newToken = data.token || data.csrf_token || data._token;
if (!newToken) {
throw new Error('No token in response');
}
this.currentToken = newToken;
this.updateAllTokens();
Logger.info('[CsrfManager] Token refreshed');
// Trigger event
this.triggerTokenRefreshedEvent(newToken);
} catch (error) {
Logger.error('[CsrfManager] Failed to refresh token', error);
throw error;
} finally {
this.isRefreshing = false;
}
}
/**
* Update all tokens on the page
*/
updateAllTokens() {
if (!this.currentToken) {
return;
}
// Update meta tag
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
metaTag.setAttribute('content', this.currentToken);
}
// Update all form inputs
const formInputs = document.querySelectorAll(`input[name="${this.config.tokenName}"]`);
formInputs.forEach(input => {
input.value = this.currentToken;
});
// Update LiveComponent tokens
const liveComponents = document.querySelectorAll('[data-live-component]');
liveComponents.forEach(element => {
const tokenInput = element.querySelector(`input[name="${this.config.tokenName}"]`);
if (tokenInput) {
tokenInput.value = this.currentToken;
}
});
}
/**
* Start auto-refresh timer
*/
startAutoRefresh() {
if (this.refreshTimer) {
return;
}
this.refreshTimer = setInterval(() => {
this.refreshToken().catch(error => {
Logger.error('[CsrfManager] Auto-refresh failed', error);
});
}, this.config.refreshInterval);
Logger.debug('[CsrfManager] Auto-refresh started', {
interval: this.config.refreshInterval
});
}
/**
* Stop auto-refresh timer
*/
stopAutoRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
}
/**
* Get token for use in fetch requests
*/
getTokenHeader() {
return {
[this.config.headerName]: this.currentToken
};
}
/**
* Trigger token refreshed event
*/
triggerTokenRefreshedEvent(token) {
const event = new CustomEvent('csrf:token-refreshed', {
detail: { token },
bubbles: true
});
window.dispatchEvent(event);
}
/**
* Destroy CSRF manager
*/
destroy() {
this.stopAutoRefresh();
this.currentToken = null;
Logger.info('[CsrfManager] Destroyed');
}
}