diff --git a/electron/database/LocalSystem.ts b/electron/database/LocalSystem.ts new file mode 100644 index 0000000..24e226a --- /dev/null +++ b/electron/database/LocalSystem.ts @@ -0,0 +1,282 @@ +import type { IpcMainInvokeEvent } from 'electron'; +import Store from 'electron-store'; + +// Electron store instance for session management +const store = new Store({ + encryptionKey: 'eritors-scribe-secure-key' +}); + +// ============================================================ +// SESSION MANAGEMENT - Retrieve userId and lang from store +// ============================================================ + +/** + * Get userId from electron-store + * Set during login via 'login-success' event + */ +function getUserIdFromSession(): string | null { + return store.get('userId', null) as string | null; +} + +/** + * Get lang from electron-store + * Set via 'set-lang' handler, defaults to 'fr' + */ +function getLangFromSession(): 'fr' | 'en' { + return store.get('userLang', 'fr') as 'fr' | 'en'; +} + +// ============================================================ +// LEGACY HANDLERS - Manual userId injection, lang must be passed +// Keep these for backward compatibility +// Updated to support Promises +// ============================================================ + +export function createDbHandler( + handler: (userId: string) => TReturn | Promise +): (event: IpcMainInvokeEvent) => Promise { + return async function(event: IpcMainInvokeEvent): Promise { + const userId = getUserIdFromSession(); + + if (!userId) { + throw new Error('User not authenticated'); + } + + try { + return await handler(userId); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`[DB] ${error.message}`); + throw error; + } + throw new Error('An unknown error occurred.'); + } + }; +} + +export function createDbHandler1( + handler: (userId: string, arg1: T1) => TReturn | Promise +): (event: IpcMainInvokeEvent, arg1: T1) => Promise { + return async function(event: IpcMainInvokeEvent, arg1: T1): Promise { + const userId = getUserIdFromSession(); + + if (!userId) { + throw new Error('User not authenticated'); + } + + try { + return await handler(userId, arg1); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`[DB] ${error.message}`); + throw error; + } + throw new Error('An unknown error occurred.'); + } + }; +} + +export function createDbHandler2( + handler: (userId: string, arg1: T1, arg2: T2) => TReturn | Promise +): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2) => Promise { + return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2): Promise { + const userId = getUserIdFromSession(); + + if (!userId) { + throw new Error('User not authenticated'); + } + + try { + return await handler(userId, arg1, arg2); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`[DB] ${error.message}`); + throw error; + } + throw new Error('An unknown error occurred.'); + } + }; +} + +export function createDbHandler3( + handler: (userId: string, arg1: T1, arg2: T2, arg3: T3) => TReturn | Promise +): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3) => Promise { + return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3): Promise { + const userId = getUserIdFromSession(); + + if (!userId) { + throw new Error('User not authenticated'); + } + + try { + return await handler(userId, arg1, arg2, arg3); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`[DB] ${error.message}`); + throw error; + } + throw new Error('An unknown error occurred.'); + } + }; +} + +// ============================================================ +// AUTO HANDLERS - Automatically inject userId AND lang +// Use these for new handlers - no need to pass lang from frontend +// ============================================================ + +/** + * Auto-handler with 0 parameters + * Automatically injects: userId, lang + * + * @example + * ipcMain.handle('db:user:get', createAutoHandler( + * function(userId: string, lang: 'fr' | 'en') { + * return User.getUser(userId, lang); + * } + * )); + * + * // Frontend call (no params needed): + * const user = await window.electron.invoke('db:user:get'); + */ +export function createAutoHandler( + handler: (userId: string, lang: 'fr' | 'en') => TReturn | Promise +): (event: IpcMainInvokeEvent) => Promise { + return async function(event: IpcMainInvokeEvent): Promise { + const userId = getUserIdFromSession(); + const lang = getLangFromSession(); + + if (!userId) { + throw new Error('User not authenticated'); + } + + try { + return await handler(userId, lang); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`[DB] ${error.message}`); + throw error; + } + throw new Error('An unknown error occurred.'); + } + }; +} + +/** + * Auto-handler with 1 parameter + * Automatically injects: userId, lang + * + * @example + * ipcMain.handle('db:book:get', createAutoHandler1( + * function(userId: string, bookId: string, lang: 'fr' | 'en') { + * return Book.getBook(bookId, userId, lang); + * } + * )); + * + * // Frontend call (only bookId needed): + * const book = await window.electron.invoke('db:book:get', bookId); + */ +export function createAutoHandler1( + handler: (userId: string, arg1: T1, lang: 'fr' | 'en') => TReturn | Promise +): (event: IpcMainInvokeEvent, arg1: T1) => Promise { + return async function(event: IpcMainInvokeEvent, arg1: T1): Promise { + const userId = getUserIdFromSession(); + const lang = getLangFromSession(); + + if (!userId) { + throw new Error('User not authenticated'); + } + + try { + return await handler(userId, arg1, lang); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`[DB] ${error.message}`); + throw error; + } + throw new Error('An unknown error occurred.'); + } + }; +} + +/** + * Auto-handler with 2 parameters + * Automatically injects: userId, lang + * + * @example + * ipcMain.handle('db:book:create', createAutoHandler1( + * function(userId: string, data: CreateBookData, lang: 'fr' | 'en') { + * return Book.addBook(null, userId, data.title, ..., lang); + * } + * )); + * + * // Frontend call (only data needed): + * const bookId = await window.electron.invoke('db:book:create', bookData); + */ +export function createAutoHandler2( + handler: (userId: string, arg1: T1, arg2: T2, lang: 'fr' | 'en') => TReturn | Promise +): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2) => Promise { + return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2): Promise { + const userId = getUserIdFromSession(); + const lang = getLangFromSession(); + + if (!userId) { + throw new Error('User not authenticated'); + } + + try { + return await handler(userId, arg1, arg2, lang); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`[DB] ${error.message}`); + throw error; + } + throw new Error('An unknown error occurred.'); + } + }; +} + +/** + * Auto-handler with 3 parameters + * Automatically injects: userId, lang + * + * @example + * ipcMain.handle('db:book:cover:update', createAutoHandler2( + * function(userId: string, bookId: string, coverImageName: string, lang: 'fr' | 'en') { + * return Book.updateBookCover(userId, bookId, coverImageName, lang); + * } + * )); + * + * // Frontend call (bookId and coverImageName needed): + * const success = await window.electron.invoke('db:book:cover:update', bookId, coverImageName); + */ +export function createAutoHandler3( + handler: (userId: string, arg1: T1, arg2: T2, arg3: T3, lang: 'fr' | 'en') => TReturn | Promise +): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3) => Promise { + return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3): Promise { + const userId = getUserIdFromSession(); + const lang = getLangFromSession(); + + if (!userId) { + throw new Error('User not authenticated'); + } + + try { + return await handler(userId, arg1, arg2, arg3, lang); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`[DB] ${error.message}`); + throw error; + } + throw new Error('An unknown error occurred.'); + } + }; +} + +export function createUniqueId(): string { + return crypto.randomUUID(); +} + +export function getCurrentDate(): string { + return new Date().toISOString(); +} diff --git a/electron/database/System.ts b/electron/database/System.ts new file mode 100644 index 0000000..b5bb0bb --- /dev/null +++ b/electron/database/System.ts @@ -0,0 +1,66 @@ +import { getDatabaseService } from './database.service.js'; +import { encryptDataWithUserKey, decryptDataWithUserKey, hashElement } from './encryption.js'; +import type { Database } from 'node-sqlite3-wasm'; +import crypto from 'crypto'; + +export default class System { + public static getDb(): Database { + const db: Database | null = getDatabaseService().getDb(); + if (!db) { + throw new Error('Database not initialized'); + } + return db; + } + + public static encryptDataWithUserKey(data: string, userKey: string): string { + return encryptDataWithUserKey(data, userKey); + } + + public static decryptDataWithUserKey(encryptedData: string, userKey: string): string { + return decryptDataWithUserKey(encryptedData, userKey); + } + + public static createUniqueId(): string { + return crypto.randomUUID(); + } + + static htmlToText(htmlNode: string): string { + let text: string = htmlNode + .replace(/<\/?p[^>]*>/gi, '\n') + .replace(//gi, '\n') + .replace(/<\/?(span|h[1-6])[^>]*>/gi, ''); + text = text + .replace(/'/g, "'") + .replace(/"/g, '"') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/'/g, "'"); + text = text.replace(/\r?\n\s*\n/g, '\n'); + text = text.replace(/[ \t]+/g, ' '); + + return text.trim(); + } + + public static getCurrentDate(): string { + return new Date().toISOString(); + } + + static dateToMySqlDate(isoDateString: string): string { + const dateObject: Date = new Date(isoDateString); + + function padWithZeroes(value: number): string { + return value.toString().padStart(2, '0'); + } + + const year: number = dateObject.getFullYear(); + const month: string = padWithZeroes(dateObject.getMonth() + 1); + const day: string = padWithZeroes(dateObject.getDate()); + + return `${year}-${month}-${day}`; + } + + public static hashElement(element: string): string { + return hashElement(element); + } +} \ No newline at end of file diff --git a/electron/ipc/book.ipc.ts b/electron/ipc/book.ipc.ts new file mode 100644 index 0000000..3be3ff2 --- /dev/null +++ b/electron/ipc/book.ipc.ts @@ -0,0 +1,507 @@ +import { ipcMain } from 'electron'; +import { + createDbHandler2, + // Auto-handlers: automatically inject userId AND lang + createAutoHandler, + createAutoHandler1 +} from '../database/LocalSystem.js'; +import Book from '../database/models/Book.js'; +import type { BookProps, GuideLine, GuideLineAI, Act, Issue, WorldProps } from '../database/models/Book.js'; + +ipcMain.handle( + 'db:book:getAll', + createAutoHandler( + async function(userId: string, lang: 'fr' | 'en') { + return await Book.getBooks(userId, lang); + } + ) +); + +ipcMain.handle( + 'db:book:get', + createAutoHandler1( + async function(userId: string, bookId: string, lang: 'fr' | 'en') { + return await Book.getBook(bookId, userId, lang); + } + ) +); +// Frontend call: await window.electron.invoke('db:book:get', bookId); + +// ============================================================ +// 3. POST /book/basic-information +// ============================================================ +interface UpdateBookBasicData { + title: string; + subTitle: string; + summary: string; + publicationDate: string; + wordCount: number; + bookId: string; +} + +ipcMain.handle( + 'db:book:updateBasicInformation', + createAutoHandler1( + function(userId: string, data: UpdateBookBasicData, lang: 'fr' | 'en') { + return Book.updateBookBasicInformation( + userId, + data.title, + data.subTitle, + data.summary, + data.publicationDate, + data.wordCount, + data.bookId, + lang + ); + } + ) +); +// Frontend call: await window.electron.invoke('db:book:updateBasicInformation', data); + +// ============================================================ +// 4. GET /book/guide-line +// ============================================================ +ipcMain.handle( + 'db:book:guideline:get', + createAutoHandler1( + async function(userId: string, bookId: string, lang: 'fr' | 'en') { + return await Book.getGuideLine(userId, bookId, lang); + } + ) +); + +// ============================================================ +// 5. POST /book/guide-line +// ============================================================ +interface UpdateGuideLineData { + bookId: string; + tone: string | null; + atmosphere: string | null; + writingStyle: string | null; + themes: string | null; + symbolism: string | null; + motifs: string | null; + narrativeVoice: string | null; + pacing: string | null; + keyMessages: string | null; + intendedAudience: string | null; +} + +ipcMain.handle( + 'db:book:guideline:update', + createAutoHandler1( + async function(userId: string, data: UpdateGuideLineData, lang: 'fr' | 'en') { + return await Book.updateGuideLine( + userId, + data.bookId, + data.tone, + data.atmosphere, + data.writingStyle, + data.themes, + data.symbolism, + data.motifs, + data.narrativeVoice, + data.pacing, + data.keyMessages, + data.intendedAudience, + lang + ); + } + ) +); + +// ============================================================ +// 6. GET /book/story +// ============================================================ +interface StoryData { + acts: Act[]; + issues: Issue[]; +} + +ipcMain.handle( + 'db:book:story:get', + createDbHandler2( + async function(userId: string, bookId: string, lang: 'fr' | 'en') { + const acts = await Book.getActsData(userId, bookId, lang); + const issues = await Book.getIssuesFromBook(userId, bookId, lang); + return { + acts, + issues + }; + } + ) +); + +// ============================================================ +// 7. POST /book/story +// TODO: Implement updateStory in Book.ts +// ============================================================ +// interface StoryUpdateData { +// bookId: string; +// acts: Act[]; +// mainChapters: ChapterProps[]; +// } +// +// ipcMain.handle( +// 'db:book:story:update', +// createDbHandler2( +// function(userId: string, data: StoryUpdateData, lang: 'fr' | 'en') { +// return Book.updateStory(userId, data.bookId, data.acts, data.mainChapters, lang); +// } +// ) +// ); + +// ============================================================ +// 8. POST /book/add +// ============================================================ +interface CreateBookData { + title: string; + subTitle: string | null; + summary: string | null; + type: string; + serieId: number | null; + desiredReleaseDate: string | null; + desiredWordCount: number | null; +} + +ipcMain.handle( + 'db:book:create', + createDbHandler2( + function(userId: string, data: CreateBookData, lang: 'fr' | 'en') { + return Book.addBook( + null, + userId, + data.title, + data.subTitle || '', + data.summary || '', + data.type, + data.serieId || 0, + data.desiredReleaseDate || '', + data.desiredWordCount || 0, + lang + ); + } + ) +); + +// ============================================================ +// 9. POST /book/cover +// ============================================================ +// TODO: Implement updateBookCover in Book.ts +// ipcMain.handle( +// 'db:book:cover:update', +// createDbHandler3( +// function(userId: string, bookId: string, coverImageName: string, lang: 'fr' | 'en') { +// return Book.updateBookCover(userId, bookId, coverImageName, lang); +// } +// ) +// ); + +// ============================================================ +// 10. POST /book/incident/new +// ============================================================ +interface AddIncidentData { + bookId: string; + name: string; +} + +ipcMain.handle( + 'db:book:incident:add', + createDbHandler2( + function(userId: string, data: AddIncidentData, lang: 'fr' | 'en') { + return Book.addNewIncident(userId, data.bookId, data.name, lang); + } + ) +); + +// ============================================================ +// 11. DELETE /book/incident/remove +// ============================================================ +interface RemoveIncidentData { + bookId: string; + incidentId: string; +} + +ipcMain.handle( + 'db:book:incident:remove', + createDbHandler2( + function(userId: string, data: RemoveIncidentData, lang: 'fr' | 'en') { + return Book.removeIncident(userId, data.bookId, data.incidentId, lang); + } + ) +); + +// ============================================================ +// 12. POST /book/plot/new +// ============================================================ +interface AddPlotPointData { + bookId: string; + name: string; + incidentId: string; +} + +ipcMain.handle( + 'db:book:plot:add', + createDbHandler2( + function(userId: string, data: AddPlotPointData, lang: 'fr' | 'en') { + return Book.addNewPlotPoint( + userId, + data.bookId, + data.incidentId, + data.name, + lang + ); + } + ) +); + +// ============================================================ +// 13. DELETE /book/plot/remove +// ============================================================ +ipcMain.handle( + 'db:book:plot:remove', + createDbHandler2( + function(userId: string, plotPointId: string, lang: 'fr' | 'en') { + return Book.removePlotPoint(userId, plotPointId, lang); + } + ) +); + +// ============================================================ +// 14. POST /book/issue/add +// ============================================================ +interface AddIssueData { + bookId: string; + name: string; +} + +ipcMain.handle( + 'db:book:issue:add', + createDbHandler2( + function(userId: string, data: AddIssueData, lang: 'fr' | 'en') { + return Book.addNewIssue(userId, data.bookId, data.name, lang); + } + ) +); + +// ============================================================ +// 15. DELETE /book/issue/remove +// ============================================================ +ipcMain.handle( + 'db:book:issue:remove', + createDbHandler2( + function(userId: string, issueId: string, lang: 'fr' | 'en') { + return Book.removeIssue(userId, issueId, lang); + } + ) +); + +// ============================================================ +// 16. GET /book/worlds +// ============================================================ +ipcMain.handle( + 'db:book:worlds:get', + createDbHandler2( + function(userId: string, bookId: string, lang: 'fr' | 'en') { + return Book.getWorlds(userId, bookId, lang); + } + ) +); + +// ============================================================ +// 17. POST /book/world/add +// ============================================================ +interface AddWorldData { + bookId: string; + worldName: string; +} + +ipcMain.handle( + 'db:book:world:add', + createDbHandler2( + function(userId: string, data: AddWorldData, lang: 'fr' | 'en') { + return Book.addNewWorld(userId, data.bookId, data.worldName, lang); + } + ) +); + +// ============================================================ +// 18. POST /book/world/element/add +// ============================================================ +interface AddWorldElementData { + worldId: string; + elementName: string; + elementType: number; +} + +ipcMain.handle( + 'db:book:world:element:add', + createDbHandler2( + function(userId: string, data: AddWorldElementData, lang: 'fr' | 'en') { + return Book.addNewElementToWorld( + userId, + data.worldId, + data.elementName, + data.elementType.toString(), + lang + ); + } + ) +); + +// ============================================================ +// 19. DELETE /book/world/element/delete +// ============================================================ +ipcMain.handle( + 'db:book:world:element:remove', + createDbHandler2( + function(userId: string, elementId: string, lang: 'fr' | 'en') { + return Book.removeElementFromWorld(userId, elementId, lang); + } + ) +); + +// ============================================================ +// 20. PUT /book/world/update +// TODO: Implement updateWorld in Book.ts +// ============================================================ +// ipcMain.handle( +// 'db:book:world:update', +// createDbHandler2( +// function(userId: string, world: WorldProps, lang: 'fr' | 'en') { +// return Book.updateWorld(userId, world, lang); +// } +// ) +// ); + +// ============================================================ +// 21. DELETE /book/cover/delete +// TODO: Implement deleteCoverPicture in Book.ts +// ============================================================ +// ipcMain.handle( +// 'db:book:cover:delete', +// createDbHandler2( +// function(userId: string, bookId: string, lang: 'fr' | 'en') { +// return Book.deleteCoverPicture(userId, bookId, lang); +// } +// ) +// ); + +// ============================================================ +// 22. DELETE /book/delete +// ============================================================ +ipcMain.handle( + 'db:book:delete', + createDbHandler2( + function(userId: string, bookId: string, lang: 'fr' | 'en') { + return Book.removeBook(userId, bookId, lang); + } + ) +); + +// ============================================================ +// 23. GET /book/ai/guideline +// ============================================================ +ipcMain.handle( + 'db:book:guideline:ai:get', + createDbHandler2( + function(userId: string, bookId: string, lang: 'fr' | 'en') { + return Book.getGuideLineAI(bookId, userId, lang); + } + ) +); + +// ============================================================ +// 24. POST /book/ai/guideline (set) +// ============================================================ +interface SetAIGuideLineData { + bookId: string; + narrativeType: number; + dialogueType: number; + globalResume: string; + atmosphere: string; + verbeTense: number; + langue: number; + themes: string; +} + +ipcMain.handle( + 'db:book:guideline:ai:set', + createDbHandler2( + function(userId: string, data: SetAIGuideLineData, lang: 'fr' | 'en') { + return Book.setAIGuideLine( + data.bookId, + userId, + data.narrativeType, + data.dialogueType, + data.globalResume, + data.atmosphere, + data.verbeTense, + data.langue, + data.themes, + lang + ); + } + ) +); + +// ============================================================ +// 25. GET /book/transform/epub +// ============================================================ +// TODO: Implement transformToEpub in Book.ts +// ipcMain.handle( +// 'db:book:export:epub', +// createDbHandler2( +// function(userId: string, bookId: string, lang: 'fr' | 'en') { +// return Book.transformToEpub(userId, bookId, lang); +// } +// ) +// ); + +// ============================================================ +// 26. GET /book/transform/pdf +// ============================================================ +// TODO: Implement transformToPDF in Book.ts +// ipcMain.handle( +// 'db:book:export:pdf', +// createDbHandler2( +// function(userId: string, bookId: string, lang: 'fr' | 'en') { +// return Book.transformToPDF(userId, bookId, lang); +// } +// ) +// ); + +// ============================================================ +// 27. GET /book/transform/docx +// ============================================================ +// TODO: Implement transformToDOCX in Book.ts +// ipcMain.handle( +// 'db:book:export:docx', +// createDbHandler2( +// function(userId: string, bookId: string, lang: 'fr' | 'en') { +// return Book.transformToDOCX(userId, bookId, lang); +// } +// ) +// ); + +// ============================================================ +// 28. GET /book/tags +// TODO: Implement getTagsFromBook in Book.ts +// ============================================================ +// interface BookTags { +// characters: Tag[]; +// locations: Tag[]; +// objects: Tag[]; +// worldElements: Tag[]; +// } +// +// ipcMain.handle( +// 'db:book:tags:get', +// createDbHandler2( +// function(userId: string, bookId: string, lang: 'fr' | 'en') { +// return Book.getTagsFromBook(userId, bookId, lang); +// } +// ) +// ); + +console.log('[IPC] Book handlers registered'); diff --git a/electron/main.ts b/electron/main.ts index 38771bc..eacf8fb 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -129,8 +129,19 @@ ipcMain.handle('remove-token', () => { return true; }); -ipcMain.on('login-success', (_event, token: string) => { +// 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(); @@ -141,6 +152,8 @@ ipcMain.on('login-success', (_event, token: string) => { ipcMain.on('logout', () => { store.delete('authToken'); + store.delete('userId'); + store.delete('userLang'); // Close database connection const db = getDatabaseService(); diff --git a/electron/preload.ts b/electron/preload.ts index 50017b4..d39cafa 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -9,8 +9,12 @@ contextBridge.exposeInMainWorld('electron', { setToken: (token: string) => ipcRenderer.invoke('set-token', token), removeToken: () => ipcRenderer.invoke('remove-token'), + // Language management + getLang: () => ipcRenderer.invoke('get-lang'), + setLang: (lang: 'fr' | 'en') => ipcRenderer.invoke('set-lang', lang), + // Auth events - loginSuccess: (token: string) => ipcRenderer.send('login-success', token), + loginSuccess: (token: string, userId: string) => ipcRenderer.send('login-success', token, userId), logout: () => ipcRenderer.send('logout'), // Database operations