- Fix WebSocket server to properly broadcast logs to all connected clients - Integrate professional logging system with real-time WebSocket interface - Add network status indicator with DigitalOcean Spaces connectivity - Implement AWS Signature V4 authentication for private bucket access - Add JSON content loader with backward compatibility to JS modules - Restore navigation breadcrumb system with comprehensive logging - Add multiple content formats: JSON + JS with automatic discovery - Enhance top bar with logger toggle and network status indicator - Remove deprecated temp-games module and clean up unused files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
269 lines
9.1 KiB
JavaScript
269 lines
9.1 KiB
JavaScript
// === CONFIGURATION ENVIRONNEMENT ===
|
|
// Configuration basée sur les variables d'environnement
|
|
|
|
class EnvConfig {
|
|
constructor() {
|
|
this.config = {
|
|
// DigitalOcean Spaces Configuration
|
|
DO_ENDPOINT: 'https://autocollant.fra1.digitaloceanspaces.com',
|
|
DO_CONTENT_PATH: 'Class_generator/ContentMe',
|
|
|
|
// Authentification DigitalOcean Spaces (depuis .env)
|
|
DO_ACCESS_KEY: 'DO801XTYPE968NZGAQM3',
|
|
DO_SECRET_KEY: '5aCCBiS9K+J8gsAe3M3/0GlliHCNjtLntwla1itCN1s',
|
|
DO_REGION: 'fra1',
|
|
|
|
// Content loading configuration - PRIORITÉ AU LOCAL
|
|
USE_REMOTE_CONTENT: true, // Activé maintenant qu'on a les clés
|
|
FALLBACK_TO_LOCAL: true, // TOUJOURS essayer local
|
|
TRY_REMOTE_FIRST: false, // Essayer local d'abord
|
|
REMOTE_TIMEOUT: 3000, // Timeout rapide pour éviter les attentes
|
|
|
|
// Debug et logging
|
|
DEBUG_MODE: true, // Activé pour debugging
|
|
LOG_CONTENT_LOADING: true
|
|
};
|
|
|
|
this.remoteContentUrl = this.buildContentUrl();
|
|
if (typeof logSh !== 'undefined') {
|
|
logSh(`🔧 EnvConfig initialisé: ${this.remoteContentUrl}`, 'INFO');
|
|
} else {
|
|
console.log('🔧 EnvConfig initialisé:', this.remoteContentUrl);
|
|
}
|
|
}
|
|
|
|
buildContentUrl() {
|
|
const endpoint = this.config.DO_ENDPOINT.replace(/\/$/, ''); // Supprimer / final si présent
|
|
const path = this.config.DO_CONTENT_PATH.replace(/^\//, ''); // Supprimer / initial si présent
|
|
return `${endpoint}/${path}/`;
|
|
}
|
|
|
|
get(key) {
|
|
return this.config[key];
|
|
}
|
|
|
|
set(key, value) {
|
|
this.config[key] = value;
|
|
|
|
// Rebuilder l'URL si changement d'endpoint ou path
|
|
if (key === 'DO_ENDPOINT' || key === 'DO_CONTENT_PATH') {
|
|
this.remoteContentUrl = this.buildContentUrl();
|
|
}
|
|
}
|
|
|
|
// Méthodes utilitaires
|
|
isRemoteContentEnabled() {
|
|
return this.config.USE_REMOTE_CONTENT;
|
|
}
|
|
|
|
isFallbackEnabled() {
|
|
return this.config.FALLBACK_TO_LOCAL;
|
|
}
|
|
|
|
isDebugMode() {
|
|
return this.config.DEBUG_MODE;
|
|
}
|
|
|
|
shouldLogContentLoading() {
|
|
return this.config.LOG_CONTENT_LOADING;
|
|
}
|
|
|
|
getRemoteContentUrl() {
|
|
return this.remoteContentUrl;
|
|
}
|
|
|
|
// Configuration dynamique depuis l'interface
|
|
updateRemoteConfig(endpoint, path) {
|
|
this.set('DO_ENDPOINT', endpoint);
|
|
this.set('DO_CONTENT_PATH', path);
|
|
logSh(`🔄 Configuration distante mise à jour: ${this.remoteContentUrl}`, 'INFO');
|
|
}
|
|
|
|
// Méthode pour tester la connectivité
|
|
async testRemoteConnection() {
|
|
try {
|
|
// Test simple avec un fichier qui devrait exister
|
|
const testUrl = `${this.remoteContentUrl}test.json`;
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), this.config.REMOTE_TIMEOUT);
|
|
|
|
const authHeaders = await this.getAuthHeaders('HEAD', testUrl);
|
|
const response = await fetch(testUrl, {
|
|
method: 'HEAD',
|
|
signal: controller.signal,
|
|
headers: authHeaders
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
return {
|
|
success: response.ok || response.status === 403, // 403 = connexion OK mais privé
|
|
status: response.status,
|
|
url: testUrl,
|
|
isPrivate: response.status === 403
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error.message,
|
|
url: `${this.remoteContentUrl}test.json`,
|
|
isTimeout: error.name === 'AbortError'
|
|
};
|
|
}
|
|
}
|
|
|
|
// Génère les headers d'authentification AWS Signature V4 pour DigitalOcean Spaces
|
|
async getAuthHeaders(method = 'HEAD', url = '') {
|
|
const headers = {};
|
|
|
|
if (this.config.DO_ACCESS_KEY && this.config.DO_SECRET_KEY) {
|
|
try {
|
|
const authHeaders = await this.generateAWSSignature(method, url);
|
|
Object.assign(headers, authHeaders);
|
|
if (typeof logSh !== 'undefined') {
|
|
logSh('🔐 Headers d\'authentification DigitalOcean générés', 'DEBUG');
|
|
}
|
|
} catch (error) {
|
|
if (typeof logSh !== 'undefined') {
|
|
logSh(`⚠️ Erreur génération signature AWS: ${error.message}`, 'ERROR');
|
|
} else {
|
|
console.warn('⚠️ Erreur génération signature AWS:', error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
// Implémentation AWS Signature V4 pour DigitalOcean Spaces
|
|
async generateAWSSignature(method, url) {
|
|
const accessKey = this.config.DO_ACCESS_KEY;
|
|
const secretKey = this.config.DO_SECRET_KEY;
|
|
const region = this.config.DO_REGION;
|
|
const service = 's3';
|
|
|
|
const now = new Date();
|
|
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
const timeStamp = now.toISOString().slice(0, 19).replace(/[-:]/g, '') + 'Z';
|
|
|
|
// Parse URL
|
|
const urlObj = new URL(url || this.remoteContentUrl);
|
|
const host = urlObj.hostname;
|
|
const canonicalUri = urlObj.pathname || '/';
|
|
const canonicalQueryString = urlObj.search ? urlObj.search.slice(1) : '';
|
|
|
|
// Canonical headers
|
|
const canonicalHeaders = `host:${host}\nx-amz-date:${timeStamp}\n`;
|
|
const signedHeaders = 'host;x-amz-date';
|
|
|
|
// Create canonical request
|
|
const payloadHash = 'UNSIGNED-PAYLOAD'; // Pour HEAD requests
|
|
const canonicalRequest = [
|
|
method,
|
|
canonicalUri,
|
|
canonicalQueryString,
|
|
canonicalHeaders,
|
|
signedHeaders,
|
|
payloadHash
|
|
].join('\n');
|
|
|
|
// Create string to sign
|
|
const algorithm = 'AWS4-HMAC-SHA256';
|
|
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
const canonicalRequestHash = await this.sha256(canonicalRequest);
|
|
const stringToSign = [
|
|
algorithm,
|
|
timeStamp,
|
|
credentialScope,
|
|
canonicalRequestHash
|
|
].join('\n');
|
|
|
|
// Calculate signature
|
|
const signingKey = await this.getSigningKey(secretKey, dateStamp, region, service);
|
|
const signatureBytes = await this.hmacSha256(signingKey, stringToSign);
|
|
const signature = this.uint8ArrayToHex(signatureBytes);
|
|
|
|
// Create authorization header
|
|
const authorization = `${algorithm} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
|
|
return {
|
|
'Authorization': authorization,
|
|
'X-Amz-Date': timeStamp,
|
|
'X-Amz-Content-Sha256': payloadHash
|
|
};
|
|
}
|
|
|
|
// Utilitaires cryptographiques avec crypto.subtle (vraie implémentation)
|
|
async sha256(message) {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(message);
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
async hmacSha256(key, message) {
|
|
const encoder = new TextEncoder();
|
|
|
|
// Si key est une string, l'encoder
|
|
let keyData;
|
|
if (typeof key === 'string') {
|
|
keyData = encoder.encode(key);
|
|
} else {
|
|
keyData = key;
|
|
}
|
|
|
|
const cryptoKey = await crypto.subtle.importKey(
|
|
'raw',
|
|
keyData,
|
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
false,
|
|
['sign']
|
|
);
|
|
|
|
const signature = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(message));
|
|
return new Uint8Array(signature);
|
|
}
|
|
|
|
async getSigningKey(secretKey, dateStamp, region, service) {
|
|
const encoder = new TextEncoder();
|
|
let key = encoder.encode('AWS4' + secretKey);
|
|
|
|
key = await this.hmacSha256(key, dateStamp);
|
|
key = await this.hmacSha256(key, region);
|
|
key = await this.hmacSha256(key, service);
|
|
key = await this.hmacSha256(key, 'aws4_request');
|
|
|
|
return key;
|
|
}
|
|
|
|
// Convertir Uint8Array en hex string
|
|
uint8ArrayToHex(uint8Array) {
|
|
return Array.from(uint8Array).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
// Diagnostic complet
|
|
getDiagnostics() {
|
|
return {
|
|
remoteContentUrl: this.remoteContentUrl,
|
|
remoteEnabled: this.isRemoteContentEnabled(),
|
|
fallbackEnabled: this.isFallbackEnabled(),
|
|
debugMode: this.isDebugMode(),
|
|
endpoint: this.config.DO_ENDPOINT,
|
|
contentPath: this.config.DO_CONTENT_PATH,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
}
|
|
|
|
// Export global
|
|
window.EnvConfig = EnvConfig;
|
|
|
|
// Instance globale
|
|
window.envConfig = new EnvConfig();
|
|
|
|
// Export Node.js (optionnel)
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = EnvConfig;
|
|
} |