import { app, BrowserWindow, ipcMain, nativeImage, protocol, safeStorage } from 'electron'; import * as path from 'path'; import * as url from 'url'; import { fileURLToPath } from 'url'; import * as fs from 'fs'; import { getDatabaseService } from './database/database.service.js'; import { getSecureStorage } from './storage/SecureStorage.js'; // Import IPC handlers import './ipc/book.ipc.js'; import './ipc/user.ipc.js'; import './ipc/chapter.ipc.js'; import './ipc/character.ipc.js'; import './ipc/location.ipc.js'; import './ipc/offline.ipc.js'; // Fix pour __dirname en ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const isDev = !app.isPackaged; // Enregistrer le protocole app:// comme standard (avant app.whenReady) if (!isDev) { protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true } } ]); } // Définir le nom de l'application app.setName('ERitors Scribe'); // En dev et prod, __dirname pointe vers dist/electron/ const preloadPath = path.join(__dirname, 'preload.js'); // Icône de l'application const iconPath = isDev ? path.join(__dirname, '../build/icon.png') : process.platform === 'darwin' ? path.join(process.resourcesPath, 'icon.icns') // macOS utilise .icns : path.join(process.resourcesPath, 'app.asar/build/icon.png'); // Windows/Linux utilisent .png let mainWindow: BrowserWindow | null = null; let loginWindow: BrowserWindow | null = null; function createLoginWindow(): void { loginWindow = new BrowserWindow({ width: 500, height: 900, resizable: false, // Ne pas définir icon sur macOS - utilise l'icône de l'app bundle ...(process.platform !== 'darwin' && { icon: iconPath }), webPreferences: { preload: preloadPath, contextIsolation: true, nodeIntegration: false, sandbox: true, }, frame: true, show: false, }); if (isDev) { const devPort = process.env.PORT || '4000'; loginWindow.loadURL(`http://localhost:${devPort}/login/login`); loginWindow.webContents.openDevTools(); } else { loginWindow.loadURL('app://./login/login/index.html'); } loginWindow.once('ready-to-show', () => { loginWindow?.show(); }); loginWindow.on('closed', () => { loginWindow = null; }); } function createMainWindow(): void { mainWindow = new BrowserWindow({ width: 1200, height: 800, // Ne pas définir icon sur macOS - utilise l'icône de l'app bundle ...(process.platform !== 'darwin' && { icon: iconPath }), webPreferences: { preload: preloadPath, contextIsolation: true, nodeIntegration: false, sandbox: true, }, show: false, }); if (isDev) { const devPort = process.env.PORT || '4000'; mainWindow.loadURL(`http://localhost:${devPort}`); mainWindow.webContents.openDevTools(); } else { mainWindow.loadURL('app://./index.html'); } mainWindow.once('ready-to-show', () => { mainWindow?.show(); }); mainWindow.on('closed', () => { mainWindow = null; }); } // IPC Handlers pour la gestion du token (OS-encrypted storage) ipcMain.handle('get-token', () => { const storage = getSecureStorage(); return storage.get('authToken', null); }); ipcMain.handle('set-token', (_event, token: string) => { const storage = getSecureStorage(); storage.set('authToken', token); return true; }); ipcMain.handle('remove-token', () => { const storage = getSecureStorage(); storage.delete('authToken'); return true; }); // IPC Handlers pour la gestion de la langue ipcMain.handle('get-lang', () => { const storage = getSecureStorage(); return storage.get('userLang', 'fr'); }); ipcMain.handle('set-lang', (_event, lang: 'fr' | 'en') => { const storage = getSecureStorage(); storage.set('userLang', lang); return true; }); // IPC Handler pour initialiser l'utilisateur après récupération depuis le serveur ipcMain.handle('init-user', async (_event, userId: string) => { console.log('[InitUser] Initializing user:', userId); const storage = getSecureStorage(); storage.set('userId', userId); storage.set('lastUserId', userId); // Save for offline mode try { const { getUserEncryptionKey, setUserEncryptionKey, hasUserEncryptionKey } = await import('./database/keyManager.js'); let encryptionKey: string | null = null; if (!hasUserEncryptionKey(userId)) { const { generateUserEncryptionKey } = await import('./database/encryption.js'); encryptionKey = generateUserEncryptionKey(userId); console.log('[InitUser] Generated new encryption key for user'); console.log('[InitUser] Key generated:', encryptionKey ? `${encryptionKey.substring(0, 10)}...` : 'UNDEFINED'); if (!encryptionKey) { console.error('[InitUser] CRITICAL: Generated key is undefined, blocking operation'); throw new Error('Failed to generate encryption key'); } setUserEncryptionKey(userId, encryptionKey); // Verify the key was saved const savedKey = getUserEncryptionKey(userId); console.log('[InitUser] Key verification after save:', savedKey ? `${savedKey.substring(0, 10)}...` : 'UNDEFINED'); if (!savedKey) { console.error('[InitUser] CRITICAL: Key was not saved correctly, blocking operation'); throw new Error('Failed to save encryption key'); } } else { encryptionKey = getUserEncryptionKey(userId); console.log('[InitUser] Using existing encryption key:', encryptionKey ? `${encryptionKey.substring(0, 10)}...` : 'UNDEFINED'); if (!encryptionKey) { console.error('[InitUser] CRITICAL: Existing key is undefined, regenerating'); const { generateUserEncryptionKey } = await import('./database/encryption.js'); encryptionKey = generateUserEncryptionKey(userId); setUserEncryptionKey(userId, encryptionKey); } } // Save userId and lastUserId to disk now that we have everything // This is the ONLY additional save after login if (safeStorage.isEncryptionAvailable()) { storage.save(); console.log('[InitUser] User ID and lastUserId saved to disk (encrypted)'); } else { console.error('[InitUser] WARNING: Cannot save user ID - encryption not available'); } return { success: true, keyCreated: !hasUserEncryptionKey(userId) }; } catch (error) { console.error('[InitUser] Error managing encryption key:', error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } }); ipcMain.on('login-success', async (_event, token: string) => { const storage = getSecureStorage(); storage.set('authToken', token); // Note: userId will be set later when we get user info from server if (loginWindow) { loginWindow.close(); } createMainWindow(); // Save AFTER mainWindow is created (fixes macOS safeStorage issue) setTimeout(async () => { try { if (safeStorage.isEncryptionAvailable()) { storage.save(); console.log('[Login] Auth token saved to disk (encrypted)'); } else { console.error('[Login] Encryption still not available after window creation'); // Try one more time after another delay setTimeout(() => { if (safeStorage.isEncryptionAvailable()) { storage.save(); console.log('[Login] Auth token saved to disk (encrypted) - second attempt'); } else { console.error('[Login] CRITICAL: Cannot encrypt credentials'); } }, 1000); } } catch (error) { console.error('[Login] Error saving auth data:', error); } }, 500); }); ipcMain.on('logout', () => { try { const storage = getSecureStorage(); // Debug: Check what's in storage before deletion console.log('[Logout] Before deletion - authToken exists:', storage.has('authToken')); console.log('[Logout] Before deletion - userId exists:', storage.has('userId')); storage.delete('authToken'); storage.delete('userId'); storage.delete('userLang'); // Debug: Check what's in storage after deletion console.log('[Logout] After deletion - authToken exists:', storage.has('authToken')); console.log('[Logout] After deletion - userId exists:', storage.has('userId')); // IMPORTANT: Save to disk to persist the deletions storage.save(); console.log('[Logout] Cleared auth data from disk'); } catch (error) { console.error('[Logout] Error clearing storage:', error); } try { const db = getDatabaseService(); db.close(); } catch (error) { console.error('[Logout] Error closing database:', error); } if (mainWindow) { mainWindow.close(); mainWindow = null; } createLoginWindow(); }); // ========== USER SYNC (PRE-AUTHENTICATION) ========== interface SyncUserData { userId: string; firstName: string; lastName: string; username: string; email: string; } ipcMain.handle('db:user:sync', async (_event, data: SyncUserData): Promise => { try { // Import User models dynamically to avoid circular dependencies const { default: User } = await import('./database/models/User.js'); const { default: UserRepo } = await import('./database/repositories/user.repository.js'); const lang: 'fr' | 'en' = 'fr'; // Check if user already exists in local DB try { UserRepo.fetchUserInfos(data.userId, lang); // User exists, no need to sync console.log(`[DB] User ${data.userId} already exists in local DB, skipping sync`); return true; } catch (error) { // User doesn't exist, create it console.log(`[DB] User ${data.userId} not found, creating in local DB`); await User.addUser( data.userId, data.firstName, data.lastName, data.username, data.email, '', lang ); console.log(`[DB] User ${data.userId} synced successfully`); return true; } } catch (error) { console.error('[DB] Failed to sync user:', error); throw error; } }); // ========== DATABASE IPC HANDLERS ========== /** * Generate user encryption key */ ipcMain.handle('generate-encryption-key', async (_event, userId: string) => { try { // Import encryption module dynamically const { generateUserEncryptionKey } = await import('./database/encryption.js'); const key = generateUserEncryptionKey(userId); return { success: true, key }; } catch (error) { console.error('Failed to generate encryption key:', error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } }); /** * Get or generate user encryption key (OS-encrypted storage) */ ipcMain.handle('get-user-encryption-key', (_event, userId: string) => { const storage = getSecureStorage(); const key = storage.get(`encryptionKey-${userId}`, null); return key; }); /** * Store user encryption key (OS-encrypted storage) */ ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey: string) => { const storage = getSecureStorage(); storage.set(`encryptionKey-${userId}`, encryptionKey); return true; }); /** * Initialize database for user */ ipcMain.handle('db-initialize', (_event, userId: string, encryptionKey: string) => { try { const db = getDatabaseService(); db.initialize(userId, encryptionKey); return { success: true }; } catch (error) { console.error('Failed to initialize database:', error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } }); // NOTE: All database IPC handlers have been moved to ./ipc/book.ipc.ts // and use the new createHandler() pattern with auto userId/lang injection app.whenReady().then(() => { // Enregistrer le protocole custom app:// pour servir les fichiers depuis out/ if (!isDev) { const outPath = path.join(process.resourcesPath, 'app.asar.unpacked/out'); protocol.handle('app', async (request) => { // Enlever app:// et ./ let filePath = request.url.replace('app://', '').replace(/^\.\//, ''); const fullPath = path.normalize(path.join(outPath, filePath)); // Vérifier que le chemin est bien dans out/ (sécurité) if (!fullPath.startsWith(outPath)) { console.error('Security: Attempted to access file outside out/:', fullPath); return new Response('Forbidden', { status: 403 }); } try { const data = await fs.promises.readFile(fullPath); const ext = path.extname(fullPath).toLowerCase(); const mimeTypes: Record = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', }; return new Response(data, { headers: { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' } }); } catch (error) { console.error('Failed to load:', fullPath, error); return new Response('Not found', { status: 404 }); } }); } // Définir l'icône du Dock sur macOS if (process.platform === 'darwin' && app.dock) { const icon = nativeImage.createFromPath(iconPath); app.dock.setIcon(icon); } // Vérifier si un token existe (OS-encrypted storage) const storage = getSecureStorage(); const token = storage.get('authToken'); const userId = storage.get('userId'); const offlineMode = storage.get('offlineMode', false); const lastUserId = storage.get('lastUserId'); const hasPin = !!storage.get(`pin-${lastUserId}`); console.log('[Startup] Token exists:', !!token); console.log('[Startup] UserId exists:', !!userId); console.log('[Startup] Offline mode:', offlineMode); console.log('[Startup] Has PIN:', hasPin); if (token) { // Token existe, ouvrir la fenêtre principale createMainWindow(); } else if (offlineMode && hasPin && lastUserId) { // Mode offline activé avec PIN, ouvrir login offline console.log('[Startup] Opening offline login page'); loginWindow = new BrowserWindow({ width: 500, height: 900, resizable: false, ...(process.platform !== 'darwin' && { icon: iconPath }), webPreferences: { preload: preloadPath, contextIsolation: true, nodeIntegration: false, sandbox: true, }, frame: true, show: false, }); if (isDev) { const devPort = process.env.PORT || '4000'; loginWindow.loadURL(`http://localhost:${devPort}/login/offline`); loginWindow.webContents.openDevTools(); } else { loginWindow.loadURL('app://./login/offline/index.html'); } loginWindow.once('ready-to-show', () => { loginWindow?.show(); }); loginWindow.on('closed', () => { loginWindow = null; }); } else { // Pas de token ou pas de mode offline, ouvrir la fenêtre de login normale createLoginWindow(); } app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { const storage = getSecureStorage(); const token = storage.get('authToken'); if (token) { createMainWindow(); } else { createLoginWindow(); } } }); }); app.on('window-all-closed', () => { // Quitter l'application quand toutes les fenêtres sont fermées app.quit(); });