From ac95e00127c36bf86c41bffa65738fda3c28931a Mon Sep 17 00:00:00 2001 From: natreex Date: Wed, 26 Nov 2025 15:25:53 -0500 Subject: [PATCH] Integrate offline support and improve error handling across app - Add `OfflineContext` to manage offline state and interactions within components. - Refactor session logic in `ScribeControllerBar` and `page.tsx` to handle offline scenarios (e.g., check connectivity before enabling GPT features). - Enhance offline PIN setup and verification with better flow and error messaging. - Optimize database IPC handlers to initialize and sync data in offline mode. - Refactor code to clean up redundant logs and ensure stricter typings. - Improve consistency and structure in handling online and offline operations for smoother user experience. --- app/page.tsx | 94 ++++++---------- components/ScribeControllerBar.tsx | 12 +- components/book/AddNewBookForm.tsx | 9 +- components/book/BookList.tsx | 14 ++- components/editor/TextEditor.tsx | 1 - electron.d.ts | 2 +- .../database/repositories/book.repository.ts | 2 +- electron/ipc/book.ipc.ts | 2 +- electron/ipc/offline.ipc.ts | 12 ++ electron/main.ts | 106 +++++------------- 10 files changed, 95 insertions(+), 159 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 9e9e04c..fba8ee5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -14,7 +14,6 @@ import {SessionContext} from '@/context/SessionContext'; import {SessionProps} from "@/lib/models/Session"; import User, {UserProps} from "@/lib/models/User"; import {BookProps} from "@/lib/models/Book"; -// Removed Next.js router imports for Electron import ScribeTopBar from "@/components/ScribeTopBar"; import ScribeControllerBar from "@/components/ScribeControllerBar"; import ScribeLeftBar from "@/components/leftbar/ScribeLeftBar"; @@ -27,7 +26,6 @@ import {faBookMedical, faFeather} from "@fortawesome/free-solid-svg-icons"; import TermsOfUse from "@/components/TermsOfUse"; import frMessages from '@/lib/locales/fr.json'; import enMessages from '@/lib/locales/en.json'; -// Removed Next.js Image import for Electron import {NextIntlClientProvider, useTranslations} from "next-intl"; import {LangContext} from "@/context/LangContext"; import {AIUsageContext} from "@/context/AIUsageContext"; @@ -45,7 +43,7 @@ function ScribeContent() { const t = useTranslations(); const {lang: locale} = useContext(LangContext); const {errorMessage} = useContext(AlertContext); - const {initializeDatabase} = useContext(OfflineContext); + const {initializeDatabase, setOfflineMode, isCurrentlyOffline} = useContext(OfflineContext); const editor: Editor | null = useEditor({ extensions: [ StarterKit, @@ -57,8 +55,7 @@ function ScribeContent() { injectCSS: false, immediatelyRender: false, }); - - // Router removed for Electron - using window.location instead + const [session, setSession] = useState({user: null, accessToken: '', isConnected: false}); const [currentChapter, setCurrentChapter] = useState(undefined); const [currentBook, setCurrentBook] = useState(null); @@ -68,8 +65,6 @@ function ScribeContent() { const [isLoading, setIsLoading] = useState(true); - const [sessionAttempts, setSessionAttempts] = useState(0) - const [isTermsAccepted, setIsTermsAccepted] = useState(false); const [homeStepsGuide, setHomeStepsGuide] = useState(false); const [showPinSetup, setShowPinSetup] = useState(false); @@ -151,12 +146,7 @@ function ScribeContent() { setIsTermsAccepted(session.user?.termsAccepted ?? false); setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic')); setIsLoading(false); - } else { - if (sessionAttempts > 2) { - // Redirect handled by checkAuthentification - } } - setSessionAttempts(sessionAttempts + 1); }, [session]); useEffect((): void => { @@ -166,54 +156,50 @@ function ScribeContent() { }, [currentBook]); // Check for PIN setup after successful connection - useEffect(() => { + useEffect(():void => { async function checkPinSetup() { if (session.isConnected && window.electron) { try { const offlineStatus = await window.electron.offlineModeGet(); - console.log('[Page] Session connected, offline status:', offlineStatus); if (!offlineStatus.hasPin) { - console.log('[Page] No PIN configured, will show setup dialog'); - // Show PIN setup dialog after a short delay - setTimeout(() => { + setTimeout(():void => { console.log('[Page] Showing PIN setup dialog'); setShowPinSetup(true); - }, 2000); // 2 seconds delay after page load + }, 2000); + } + } catch (e:unknown) { + if (e instanceof Error) { + errorMessage(e.message); + } else { + errorMessage('Unknown error occurred while checking offline mode') } - } catch (error) { - console.error('[Page] Error checking offline mode:', error); } } } - checkPinSetup(); - }, [session.isConnected]); // Run when session connection status changes + checkPinSetup().then(); + }, [session.isConnected]); async function handlePinVerifySuccess(userId: string): Promise { console.log('[OfflinePin] PIN verified successfully for user:', userId); try { - // Initialize database with user's encryption key if (window.electron) { const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId); if (encryptionKey) { await window.electron.dbInitialize(userId, encryptionKey); - - // Load user from local DB - const localUser = await window.electron.invoke('db:user:info'); - if (localUser && localUser.success) { - // Use local data and continue in offline mode + + const localUser:UserProps = await window.electron.invoke('db:user:info'); + if (localUser && localUser.id) { setSession({ isConnected: true, - user: localUser.data, + user: localUser, accessToken: 'offline', // Special offline token }); setShowPinVerify(false); - setCurrentCredits(localUser.data.creditsBalance || 0); - setAmountSpent(localUser.data.aiUsage || 0); - - console.log('[OfflinePin] Running in offline mode'); + setCurrentCredits(localUser.creditsBalance || 0); + setAmountSpent(localUser.aiUsage || 0); } else { errorMessage(t("homePage.errors.localDataError")); if (window.electron) { @@ -282,29 +268,18 @@ function ScribeContent() { return; } - console.log('user: ' , user); - - // Initialize user in Electron (sets userId and creates/gets encryption key) if (window.electron && user.id) { try { const initResult = await window.electron.initUser(user.id); if (!initResult.success) { console.error('[Page] Failed to initialize user:', initResult.error); } else { - console.log('[Page] User initialized successfully, key created:', initResult.keyCreated); - - // Check if PIN is configured for offline mode try { const offlineStatus = await window.electron.offlineModeGet(); - console.log('[Page] Offline status:', offlineStatus); if (!offlineStatus.hasPin) { - // First login or no PIN configured yet - // Show PIN setup dialog after a short delay - console.log('[Page] No PIN configured, will show setup dialog'); - setTimeout(() => { - console.log('[Page] Showing PIN setup dialog'); + setTimeout(():void => { setShowPinSetup(true); - }, 2000); // 2 seconds delay after successful login + }, 2000); } } catch (error) { console.error('[Page] Error checking offline mode:', error); @@ -321,15 +296,10 @@ function ScribeContent() { }); setCurrentCredits(user.creditsBalance) setAmountSpent(user.aiUsage) - - // Initialiser la DB locale en Electron if (window.electron && user.id) { try { - const dbInitialized = await initializeDatabase(user.id); + const dbInitialized:boolean = await initializeDatabase(user.id); if (dbInitialized) { - console.log('Database initialized successfully'); - - // Sync user to local DB (only if not exists) try { await window.electron.invoke('db:user:sync', { userId: user.id, @@ -341,7 +311,6 @@ function ScribeContent() { console.log('User synced to local DB'); } catch (syncError) { console.error('Failed to sync user to local DB:', syncError); - // Non-blocking error, continue anyway } } } catch (error) { @@ -349,13 +318,12 @@ function ScribeContent() { } } } catch (e: unknown) { - console.log('[Auth] Server error, checking offline mode...'); if (window.electron) { try { const offlineStatus = await window.electron.offlineModeGet(); if (offlineStatus.hasPin && offlineStatus.lastUserId) { - console.log('[Auth] Server unreachable but PIN configured, showing PIN verification'); + setOfflineMode(prev => ({...prev, isOffline: true, isNetworkOnline: false})); setShowPinVerify(true); setIsLoading(false); return; @@ -369,8 +337,7 @@ function ScribeContent() { console.error('[Auth] Error checking offline mode:', offlineError); } } - - // If not in offline mode or no PIN configured, show error and logout + if (e instanceof Error) { errorMessage(e.message); } else { @@ -383,7 +350,7 @@ function ScribeContent() { const offlineStatus = await window.electron.offlineModeGet(); if (offlineStatus.hasPin && offlineStatus.lastUserId) { - console.log('[Auth] No token but PIN configured, showing PIN verification for offline mode'); + setOfflineMode(prev => ({...prev, isOffline: true, isNetworkOnline: false})); setShowPinVerify(true); setIsLoading(false); return; @@ -428,7 +395,12 @@ function ScribeContent() { async function getLastChapter(): Promise { if (session?.accessToken) { try { - const response: ChapterProps | null = await System.authGetQueryToServer(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId}); + let response: ChapterProps | null + if (isCurrentlyOffline()){ + response = await window.electron.invoke('db:chapter:last', currentBook?.bookId) + } else { + response = await System.authGetQueryToServer(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId}); + } if (response) { setCurrentChapter(response) } else { @@ -489,12 +461,12 @@ function ScribeContent() { { - homeStepsGuide && + homeStepsGuide && !isCurrentlyOffline() && setHomeStepsGuide(false)}/> } { - !isTermsAccepted && + !isTermsAccepted && !isCurrentlyOffline() && } { showPinSetup && window.electron && ( diff --git a/components/ScribeControllerBar.tsx b/components/ScribeControllerBar.tsx index 8ef5a34..ca81a9b 100644 --- a/components/ScribeControllerBar.tsx +++ b/components/ScribeControllerBar.tsx @@ -17,6 +17,7 @@ import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import CreditCounter from "@/components/CreditMeters"; import QuillSense from "@/lib/models/QuillSense"; +import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; export default function ScribeControllerBar() { const {chapter, setChapter} = useContext(ChapterContext); @@ -24,12 +25,13 @@ export default function ScribeControllerBar() { const {errorMessage} = useContext(AlertContext) const {session} = useContext(SessionContext); const t = useTranslations(); - const {lang, setLang} = useContext(LangContext) + const {lang, setLang} = useContext(LangContext); + const {isCurrentlyOffline} = useContext(OfflineContext) - const isGPTEnabled: boolean = QuillSense.isOpenAIEnabled(session); - const isGemini: boolean = QuillSense.isOpenAIEnabled(session); - const isAnthropic: boolean = QuillSense.isOpenAIEnabled(session); - const isSubTierTwo: boolean = QuillSense.getSubLevel(session) >= 2; + const isGPTEnabled: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session); + const isGemini: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session); + const isAnthropic: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session); + const isSubTierTwo: boolean = !isCurrentlyOffline() && QuillSense.getSubLevel(session) >= 2; const hasAccess: boolean = (isGPTEnabled || isAnthropic || isGemini) || isSubTierTwo; const [showSettingPanel, setShowSettingPanel] = useState(false); diff --git a/components/book/AddNewBookForm.tsx b/components/book/AddNewBookForm.tsx index b9ff37e..f123766 100644 --- a/components/book/AddNewBookForm.tsx +++ b/components/book/AddNewBookForm.tsx @@ -146,13 +146,14 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch< publicationDate: publicationDate, desiredWordCount: wordCount, }, token, lang); - if (!bookId) { - throw new Error(t('addNewBookForm.error.addingBook')); - } } else { - // Offline - call local database bookId = await window.electron.invoke('db:book:create', bookData); } + + if (!bookId) { + errorMessage(t('addNewBookForm.error.addingBook')) + return; + } const book: BookProps = { bookId: bookId, diff --git a/components/book/BookList.tsx b/components/book/BookList.tsx index 9c918c9..382b9e2 100644 --- a/components/book/BookList.tsx +++ b/components/book/BookList.tsx @@ -172,12 +172,14 @@ export default function BookList() { async function getBook(bookId: string): Promise { try { - const bookResponse: BookListProps = await System.authGetQueryToServer( - `book/basic-information`, - accessToken, - lang, - {id: bookId} - ); + let bookResponse: BookListProps|null = null; + if (isCurrentlyOffline()){ + bookResponse = await window.electron.invoke('db:book:bookBasicInformation', bookId) + } else { + bookResponse = await System.authGetQueryToServer(`book/basic-information`, accessToken, lang, { + id: bookId + }); + } if (!bookResponse) { errorMessage(t("bookList.errorBookDetails")); return; diff --git a/components/editor/TextEditor.tsx b/components/editor/TextEditor.tsx index e7e2590..d8d6e06 100644 --- a/components/editor/TextEditor.tsx +++ b/components/editor/TextEditor.tsx @@ -398,7 +398,6 @@ export default function TextEditor() { const parsedContent = JSON.parse(chapter.chapterContent.content); editor.commands.setContent(parsedContent); } catch (error) { - console.error('Erreur lors du parsing du contenu:', error); editor.commands.setContent({ type: "doc", content: [{type: "paragraph", content: []}] diff --git a/electron.d.ts b/electron.d.ts index 19c2b4a..4bb2145 100644 --- a/electron.d.ts +++ b/electron.d.ts @@ -11,7 +11,7 @@ export interface IElectronAPI { platform: NodeJS.Platform; // Generic invoke method - use this for all IPC calls - invoke: (channel: string, ...args: any[]) => Promise; + invoke: (channel: string, ...args: any[]) => Promise; // Token management (shortcuts for convenience) getToken: () => Promise; diff --git a/electron/database/repositories/book.repository.ts b/electron/database/repositories/book.repository.ts index 3e99ab1..a93479f 100644 --- a/electron/database/repositories/book.repository.ts +++ b/electron/database/repositories/book.repository.ts @@ -259,7 +259,7 @@ export default class BookRepo { let result:RunResult try { const db: Database = System.getDb(); - result = db.run('INSERT INTO erit_books (book_id,type,author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, erit_books.desired_word_count) VALUES (?,?,?,?,?,?,?,?,?,?,?)', [bookId, type, userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, serie, publicationDate ? publicationDate : null, desiredWordCount]); + result = db.run('INSERT INTO erit_books (book_id, type, author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count) VALUES (?,?,?,?,?,?,?,?,?,?,?)', [bookId, type, userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, serie, publicationDate ? publicationDate : null, desiredWordCount]); } catch (err: unknown) { if (err instanceof Error) { console.error(`DB Error: ${err.message}`); diff --git a/electron/ipc/book.ipc.ts b/electron/ipc/book.ipc.ts index 949f506..232583b 100644 --- a/electron/ipc/book.ipc.ts +++ b/electron/ipc/book.ipc.ts @@ -97,7 +97,7 @@ ipcMain.handle('db:book:books', createHandler( // GET /book/:id - Get single book ipcMain.handle('db:book:bookBasicInformation', createHandler( async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise { - return await Book.getBook(bookId, userId); + return await Book.getBook(userId, bookId); } ) ); diff --git a/electron/ipc/offline.ipc.ts b/electron/ipc/offline.ipc.ts index e30c9c4..b86ee05 100644 --- a/electron/ipc/offline.ipc.ts +++ b/electron/ipc/offline.ipc.ts @@ -2,6 +2,7 @@ import { ipcMain } from 'electron'; import { createHandler } from '../database/LocalSystem.js'; import * as bcrypt from 'bcrypt'; import { getSecureStorage } from '../storage/SecureStorage.js'; +import { getDatabaseService } from '../database/database.service.js'; interface SetPinData { pin: string; @@ -62,6 +63,17 @@ ipcMain.handle('offline:pin:verify', async (_event, data: VerifyPinData) => { if (isValid) { // Set userId for session storage.set('userId', lastUserId); + + // Initialize database for offline use + const encryptionKey = storage.get(`encryptionKey-${lastUserId}`); + if (encryptionKey) { + const db = getDatabaseService(); + db.initialize(lastUserId, encryptionKey); + } else { + console.error('[Offline] No encryption key found for user'); + return { success: false, error: 'No encryption key found' }; + } + console.log('[Offline] PIN verified, user authenticated locally'); return { success: true, diff --git a/electron/main.ts b/electron/main.ts index f5c71a0..3edfc6c 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,12 +1,11 @@ -import { app, BrowserWindow, ipcMain, nativeImage, protocol, safeStorage } from 'electron'; +import {app, BrowserWindow, ipcMain, nativeImage, protocol, safeStorage} from 'electron'; import * as path from 'path'; -import * as url from 'url'; -import { fileURLToPath } 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 { getUserEncryptionKey, setUserEncryptionKey, hasUserEncryptionKey } from './database/keyManager.js'; -import { generateUserEncryptionKey } from './database/encryption.js'; +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'; @@ -58,7 +57,6 @@ function createLoginWindow(): void { 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, @@ -91,7 +89,6 @@ 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, @@ -121,11 +118,8 @@ function createMainWindow(): void { // IPC Handlers pour la gestion du token (OS-encrypted storage) ipcMain.handle('get-token', () => { - const storage = getSecureStorage(); - const token = storage.get('authToken', null); - console.log('[GetToken] Token requested, exists:', !!token); - console.log('[GetToken] Storage has authToken:', storage.has('authToken')); - return token; + const storage:SecureStorage = getSecureStorage(); + return storage.get('authToken', null); }); ipcMain.handle('set-token', (_event, token: string) => { @@ -154,11 +148,9 @@ ipcMain.handle('set-lang', (_event, lang: 'fr' | 'en') => { // 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(); + const storage:SecureStorage = getSecureStorage(); storage.set('userId', userId); - storage.set('lastUserId', userId); // Save for offline mode + storage.set('lastUserId', userId); try { let encryptionKey: string | null = null; @@ -166,22 +158,16 @@ ipcMain.handle('init-user', async (_event, userId: string) => { if (!hasUserEncryptionKey(userId)) { 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); + + const savedKey:string = 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 { @@ -195,9 +181,7 @@ ipcMain.handle('init-user', async (_event, userId: string) => { 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)'); @@ -219,28 +203,21 @@ ipcMain.on('login-success', async (_event, token: string) => { console.log('[Login] Received token, setting in storage'); const storage = getSecureStorage(); storage.set('authToken', token); - console.log('[Login] Token set in cache, has authToken:', storage.has('authToken')); - // 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'); } @@ -252,31 +229,21 @@ ipcMain.on('login-success', async (_event, token: string) => { }, 500); }); -ipcMain.on('logout', () => { +ipcMain.on('logout', ():void => { 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')); + const storage:SecureStorage = getSecureStorage(); 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(); + const db:DatabaseService = getDatabaseService(); db.close(); } catch (error) { console.error('[Logout] Error closing database:', error); @@ -306,17 +273,10 @@ ipcMain.handle('db:user:sync', async (_event, data: SyncUserData): Promise { try { - const key = generateUserEncryptionKey(userId); + const key:string = generateUserEncryptionKey(userId); return { success: true, key }; } catch (error) { console.error('Failed to generate encryption key:', error); @@ -357,16 +314,15 @@ ipcMain.handle('generate-encryption-key', async (_event, userId: string) => { * 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; + 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 = getSecureStorage(); + const storage:SecureStorage = getSecureStorage(); storage.set(`encryptionKey-${userId}`, encryptionKey); return true; }); @@ -376,7 +332,7 @@ ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey */ ipcMain.handle('db-initialize', (_event, userId: string, encryptionKey: string) => { try { - const db = getDatabaseService(); + const db:DatabaseService = getDatabaseService(); db.initialize(userId, encryptionKey); return { success: true }; } catch (error) { @@ -388,28 +344,21 @@ ipcMain.handle('db-initialize', (_event, userId: string, encryptionKey: string) } }); -// 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é) + let filePath:string = request.url.replace('app://', '').replace(/^\.\//, ''); + const fullPath:string = path.normalize(path.join(outPath, filePath)); + 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 ext:string = path.extname(fullPath).toLowerCase(); const mimeTypes: Record = { '.html': 'text/html', '.css': 'text/css', @@ -428,7 +377,6 @@ app.whenReady().then(() => { headers: { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' } }); } catch (error) { - console.error('Failed to load:', fullPath, error); return new Response('Not found', { status: 404 }); } });