import {app, BrowserWindow, ipcMain, nativeImage, protocol, safeStorage, shell} from 'electron'; import * as path from 'path'; import {fileURLToPath} from 'url'; import * as fs from 'fs'; import {DatabaseService, getDatabaseService} from './database/database.service.js'; import SecureStorage, {getSecureStorage} from './storage/SecureStorage.js'; import {getUserEncryptionKey, hasUserEncryptionKey, setUserEncryptionKey} from './database/keyManager.js'; import {generateUserEncryptionKey} from './database/encryption.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, ...(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, ...(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 Handler pour ouvrir des liens externes (navigateur/app native) ipcMain.handle('open-external', async (_event, url: string) => { await shell.openExternal(url); }); // IPC Handlers pour la gestion du token (OS-encrypted storage) ipcMain.handle('get-token', () => { const storage:SecureStorage = 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) => { const storage:SecureStorage = getSecureStorage(); storage.set('userId', userId); storage.set('lastUserId', userId); try { let encryptionKey: string | null = null; if (!hasUserEncryptionKey(userId)) { encryptionKey = generateUserEncryptionKey(userId); if (!encryptionKey) { throw new Error('Failed to generate encryption key'); } setUserEncryptionKey(userId, encryptionKey); const savedKey:string = getUserEncryptionKey(userId); console.log('[InitUser] Key verification after save:', savedKey ? `${savedKey.substring(0, 10)}...` : 'UNDEFINED'); if (!savedKey) { 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); } } 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) => { console.log('[Login] Received token, setting in storage'); const storage = getSecureStorage(); storage.set('authToken', token); if (loginWindow) { loginWindow.close(); } createMainWindow(); setTimeout(async () => { try { if (safeStorage.isEncryptionAvailable()) { storage.save(); } else { setTimeout(() => { if (safeStorage.isEncryptionAvailable()) { storage.save(); } else { console.error('[Login] CRITICAL: Cannot encrypt credentials'); } }, 1000); } } catch (error) { console.error('[Login] Error saving auth data:', error); } }, 500); }); ipcMain.on('logout', ():void => { try { const storage:SecureStorage = getSecureStorage(); storage.delete('authToken'); storage.delete('userId'); storage.delete('userLang'); storage.save(); } catch (error) { console.error('[Logout] Error clearing storage:', error); } try { const db:DatabaseService = 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 { const { default: User } = await import('./database/models/User.js'); const { default: UserRepo } = await import('./database/repositories/user.repository.js'); const lang: 'fr' | 'en' = 'fr'; try { UserRepo.fetchUserInfos(data.userId, lang); return true; } catch (error) { await User.addUser( data.userId, data.firstName, data.lastName, data.username, data.email, '', lang ); return true; } } catch (error) { console.error('[DB] Failed to sync user:', error); throw error; } }); /** * Generate user encryption key */ ipcMain.handle('generate-encryption-key', async (_event, userId: string) => { try { const key:string = 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:SecureStorage = getSecureStorage(); return storage.get(`encryptionKey-${userId}`, null); }); /** * Store user encryption key (OS-encrypted storage) */ ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey: string) => { const storage:SecureStorage = getSecureStorage(); storage.set(`encryptionKey-${userId}`, encryptionKey); return true; }); /** * Initialize database for user */ ipcMain.handle('db-initialize', (_event, userId: string, encryptionKey: string) => { try { const db:DatabaseService = 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' }; } }); app.whenReady().then(() => { if (!isDev) { const outPath = path.join(process.resourcesPath, 'app.asar.unpacked/out'); protocol.handle('app', async (request) => { let filePath:string = request.url.replace('app://', '').replace(/^\.\//, ''); const fullPath:string = path.normalize(path.join(outPath, filePath)); if (!fullPath.startsWith(outPath)) { return new Response('Forbidden', { status: 403 }); } try { const data = await fs.promises.readFile(fullPath); const ext:string = 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) { 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) { createMainWindow(); } else { 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(); });