import { app, BrowserWindow, ipcMain, nativeImage, protocol } from 'electron'; import * as path from 'path'; import * as url from 'url'; import { fileURLToPath } from 'url'; import Store from 'electron-store'; import * as fs from 'fs'; import { getDatabaseService } from './database/database.service.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'; // 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 // Store sécurisé pour le token const store = new Store({ encryptionKey: 'eritors-scribe-secure-key' // En production, utiliser une clé générée }); 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 ipcMain.handle('get-token', () => { return store.get('authToken', null); }); ipcMain.handle('set-token', (_event, token: string) => { store.set('authToken', token); return true; }); ipcMain.handle('remove-token', () => { store.delete('authToken'); return true; }); // IPC Handlers pour la gestion de la langue ipcMain.handle('get-lang', () => { return store.get('userLang', 'fr'); }); ipcMain.handle('set-lang', (_event, lang: 'fr' | 'en') => { store.set('userLang', lang); return true; }); ipcMain.on('login-success', (_event, token: string, userId: string) => { store.set('authToken', token); store.set('userId', userId); if (loginWindow) { loginWindow.close(); } createMainWindow(); }); ipcMain.on('logout', () => { store.delete('authToken'); store.delete('userId'); store.delete('userLang'); // Close database connection const db = getDatabaseService(); db.close(); if (mainWindow) { mainWindow.close(); } 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 */ ipcMain.handle('get-user-encryption-key', (_event, userId: string) => { const key = store.get(`encryptionKey-${userId}`, null); return key; }); /** * Store user encryption key */ ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey: string) => { store.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 const token = store.get('authToken'); console.log('Token exists:', !!token); if (token) { // Token existe, ouvrir la fenêtre principale createMainWindow(); } else { // Pas de token, ouvrir la fenêtre de login createLoginWindow(); } app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { const token = store.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(); });