// === CONFIGURATION ENVIRONNEMENT === // Configuration basée sur les variables d'environnement class EnvConfig { constructor() { // Détecter le mode file:// et désactiver les services externes const isFileMode = window.location.protocol === 'file:'; this.config = { // DigitalOcean Spaces Configuration DO_ENDPOINT: 'https://autocollant.fra1.digitaloceanspaces.com', DO_CONTENT_PATH: 'Class_generator/ContentMe', // Authentification DigitalOcean Spaces (clé avec listing) DO_ACCESS_KEY: 'DO8018LC8QF7CFBF7E2K', DO_SECRET_KEY: 'RLH4bUidH4zb1XQAtBUeUnA4vjizdkQ78D1fOZ5gYpk', DO_REGION: 'fra1', // Content loading configuration - AUTO-DÉSACTIVÉ en mode file:// USE_REMOTE_CONTENT: !isFileMode, // Désactivé en mode file:// FALLBACK_TO_LOCAL: true, // TOUJOURS essayer local TRY_REMOTE_FIRST: !isFileMode, // Désactivé en mode file:// REMOTE_TIMEOUT: 3000, // Timeout rapide pour éviter les attentes // Debug et logging DEBUG_MODE: true, // Activé pour debugging LOG_CONTENT_LOADING: true, // Mode détecté IS_FILE_MODE: isFileMode }; this.remoteContentUrl = this.buildContentUrl(); if (typeof logSh !== 'undefined') { if (isFileMode) { logSh(`📁 EnvConfig en mode file:// - Services distants désactivés`, 'INFO'); } else { logSh(`🔧 EnvConfig initialisé: ${this.remoteContentUrl}`, 'INFO'); } } else { if (isFileMode) { console.log('📁 EnvConfig en mode file:// - Services distants désactivés'); } 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() { // Ne pas tester en mode file:// if (window.location.protocol === 'file:') { return { success: false, status: 0, url: 'file://', error: 'Mode file:// - test distant ignoré' }; } try { // UTILISER LE PROXY LOCAL au lieu de DigitalOcean directement const testUrl = `http://localhost:8083/do-proxy/english-class-demo.json`; logSh(`🔍 Test de connectivité via proxy: ${testUrl}`, 'INFO'); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.REMOTE_TIMEOUT); const response = await fetch(testUrl, { method: 'GET', signal: controller.signal, mode: 'cors' }); 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: `http://localhost:8083/do-proxy/english-class-demo.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 = method === 'GET' ? await this.sha256('') : 'UNSIGNED-PAYLOAD'; 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; // Export Node.js (optionnel) if (typeof module !== 'undefined' && module.exports) { module.exports = EnvConfig; } // Initialisation globale pour le navigateur if (typeof window !== 'undefined') { window.envConfig = new EnvConfig(); }