Class_generator/Legacy/js/core/env-config.js
StillHammer 38920cc858 Complete architectural rewrite with ultra-modular system
Major Changes:
- Moved legacy system to Legacy/ folder for archival
- Built new modular architecture with strict separation of concerns
- Created core system: Module, EventBus, ModuleLoader, Router
- Added Application bootstrap with auto-start functionality
- Implemented development server with ES6 modules support
- Created comprehensive documentation and project context
- Converted SBS-7-8 content to JSON format
- Copied all legacy games and content to new structure

New Architecture Features:
- Sealed modules with WeakMap private data
- Strict dependency injection system
- Event-driven communication only
- Inviolable responsibility patterns
- Auto-initialization without commands
- Component-based UI foundation ready

Technical Stack:
- Vanilla JS/HTML/CSS only
- ES6 modules with proper imports/exports
- HTTP development server (no file:// protocol)
- Modular CSS with component scoping
- Comprehensive error handling and debugging

Ready for Phase 2: Converting legacy modules to new architecture

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 07:08:39 +08:00

297 lines
10 KiB
JavaScript

// === 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();
}