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>
297 lines
10 KiB
JavaScript
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();
|
|
} |