- Add comprehensive IPC handlers for database operations like books, chapters, characters, and conversations. - Implement local database initialization and user data encryption. - Update preload script paths for consistent environment handling. - Modify `page.tsx` to initialize local database within Electron environment. - Add new dependencies including `node-sqlite3-wasm` and `electron-rebuild`.
477 lines
12 KiB
TypeScript
477 lines
12 KiB
TypeScript
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';
|
|
|
|
// 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;
|
|
});
|
|
|
|
ipcMain.on('login-success', (_event, token: string) => {
|
|
store.set('authToken', token);
|
|
|
|
if (loginWindow) {
|
|
loginWindow.close();
|
|
}
|
|
|
|
createMainWindow();
|
|
});
|
|
|
|
ipcMain.on('logout', () => {
|
|
store.delete('authToken');
|
|
|
|
// Close database connection
|
|
const db = getDatabaseService();
|
|
db.close();
|
|
|
|
if (mainWindow) {
|
|
mainWindow.close();
|
|
}
|
|
|
|
createLoginWindow();
|
|
});
|
|
|
|
// ========== 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'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get all books
|
|
*/
|
|
ipcMain.handle('db-get-books', () => {
|
|
try {
|
|
const db = getDatabaseService();
|
|
const books = db.getBooks();
|
|
return { success: true, data: books };
|
|
} catch (error) {
|
|
console.error('Failed to get books:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get single book with all data
|
|
*/
|
|
ipcMain.handle('db-get-book', (_event, bookId: string) => {
|
|
try {
|
|
const db = getDatabaseService();
|
|
const book = db.getBook(bookId);
|
|
return { success: true, data: book };
|
|
} catch (error) {
|
|
console.error('Failed to get book:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Save book
|
|
*/
|
|
ipcMain.handle('db-save-book', (_event, book: any, authorId?: string) => {
|
|
try {
|
|
const db = getDatabaseService();
|
|
db.saveBook(book, authorId);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Failed to save book:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Delete book
|
|
*/
|
|
ipcMain.handle('db-delete-book', (_event, bookId: string) => {
|
|
try {
|
|
const db = getDatabaseService();
|
|
db.deleteBook(bookId);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Failed to delete book:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Save chapter
|
|
*/
|
|
ipcMain.handle('db-save-chapter', (_event, chapter: any, bookId: string, contentId?: string) => {
|
|
try {
|
|
const db = getDatabaseService();
|
|
db.saveChapter(chapter, bookId, contentId);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Failed to save chapter:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get characters for a book
|
|
*/
|
|
ipcMain.handle('db-get-characters', (_event, bookId: string) => {
|
|
try {
|
|
const db = getDatabaseService();
|
|
const characters = db.getCharacters(bookId);
|
|
return { success: true, data: characters };
|
|
} catch (error) {
|
|
console.error('Failed to get characters:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Save character
|
|
*/
|
|
ipcMain.handle('db-save-character', (_event, character: any, bookId: string) => {
|
|
try {
|
|
const db = getDatabaseService();
|
|
db.saveCharacter(character, bookId);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Failed to save character:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get AI conversations for a book
|
|
*/
|
|
ipcMain.handle('db-get-conversations', (_event, bookId: string) => {
|
|
try {
|
|
const db = getDatabaseService();
|
|
const conversations = db.getConversations(bookId);
|
|
return { success: true, data: conversations };
|
|
} catch (error) {
|
|
console.error('Failed to get conversations:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Save AI conversation
|
|
*/
|
|
ipcMain.handle('db-save-conversation', (_event, conversation: any, bookId: string) => {
|
|
try {
|
|
const db = getDatabaseService();
|
|
db.saveConversation(conversation, bookId);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Failed to save conversation:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get sync status
|
|
*/
|
|
ipcMain.handle('db-get-sync-status', () => {
|
|
try {
|
|
const db = getDatabaseService();
|
|
const status = db.getSyncStatus();
|
|
return { success: true, data: status };
|
|
} catch (error) {
|
|
console.error('Failed to get sync status:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get pending changes
|
|
*/
|
|
ipcMain.handle('db-get-pending-changes', (_event, limit: number = 100) => {
|
|
try {
|
|
const db = getDatabaseService();
|
|
const changes = db.getPendingChanges(limit);
|
|
return { success: true, data: changes };
|
|
} catch (error) {
|
|
console.error('Failed to get pending changes:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
});
|
|
|
|
app.whenReady().then(() => {
|
|
console.log('App ready, isDev:', isDev);
|
|
console.log('resourcesPath:', process.resourcesPath);
|
|
console.log('isPackaged:', app.isPackaged);
|
|
|
|
// 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<string, string> = {
|
|
'.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();
|
|
});
|