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.
This commit is contained in:
natreex
2025-11-26 15:25:53 -05:00
parent 5cceceaea9
commit ac95e00127
10 changed files with 95 additions and 159 deletions

View File

@@ -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<SessionProps>({user: null, accessToken: '', isConnected: false});
const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined);
const [currentBook, setCurrentBook] = useState<BookProps | null>(null);
@@ -68,8 +65,6 @@ function ScribeContent() {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [sessionAttempts, setSessionAttempts] = useState<number>(0)
const [isTermsAccepted, setIsTermsAccepted] = useState<boolean>(false);
const [homeStepsGuide, setHomeStepsGuide] = useState<boolean>(false);
const [showPinSetup, setShowPinSetup] = useState<boolean>(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<void> {
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<void> {
if (session?.accessToken) {
try {
const response: ChapterProps | null = await System.authGetQueryToServer<ChapterProps | null>(`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<ChapterProps | null>(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId});
}
if (response) {
setCurrentChapter(response)
} else {
@@ -489,12 +461,12 @@ function ScribeContent() {
</EditorContext.Provider>
</div>
{
homeStepsGuide &&
homeStepsGuide && !isCurrentlyOffline() &&
<GuideTour stepId={0} steps={homeSteps} onComplete={handleHomeTour}
onClose={(): void => setHomeStepsGuide(false)}/>
}
{
!isTermsAccepted && <TermsOfUse onAccept={handleTermsAcceptance}/>
!isTermsAccepted && !isCurrentlyOffline() && <TermsOfUse onAccept={handleTermsAcceptance}/>
}
{
showPinSetup && window.electron && (

View File

@@ -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<LangContextProps>(LangContext)
const {lang, setLang} = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(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<boolean>(false);

View File

@@ -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<string>('db:book:create', bookData);
}
if (!bookId) {
errorMessage(t('addNewBookForm.error.addingBook'))
return;
}
const book: BookProps = {
bookId: bookId,

View File

@@ -172,12 +172,14 @@ export default function BookList() {
async function getBook(bookId: string): Promise<void> {
try {
const bookResponse: BookListProps = await System.authGetQueryToServer<BookListProps>(
`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<BookListProps>(`book/basic-information`, accessToken, lang, {
id: bookId
});
}
if (!bookResponse) {
errorMessage(t("bookList.errorBookDetails"));
return;

View File

@@ -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: []}]

2
electron.d.ts vendored
View File

@@ -11,7 +11,7 @@ export interface IElectronAPI {
platform: NodeJS.Platform;
// Generic invoke method - use this for all IPC calls
invoke: <T = any>(channel: string, ...args: any[]) => Promise<T>;
invoke: <T>(channel: string, ...args: any[]) => Promise<T>;
// Token management (shortcuts for convenience)
getToken: () => Promise<string | null>;

View File

@@ -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}`);

View File

@@ -97,7 +97,7 @@ ipcMain.handle('db:book:books', createHandler<void, BookProps[]>(
// GET /book/:id - Get single book
ipcMain.handle('db:book:bookBasicInformation', createHandler<string, BookProps>(
async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise<BookProps> {
return await Book.getBook(bookId, userId);
return await Book.getBook(userId, bookId);
}
)
);

View File

@@ -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<string>(`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,

View File

@@ -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<boole
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,
@@ -326,7 +286,6 @@ ipcMain.handle('db:user:sync', async (_event, data: SyncUserData): Promise<boole
'',
lang
);
console.log(`[DB] User ${data.userId} synced successfully`);
return true;
}
} catch (error) {
@@ -335,14 +294,12 @@ ipcMain.handle('db:user:sync', async (_event, data: SyncUserData): Promise<boole
}
});
// ========== DATABASE IPC HANDLERS ==========
/**
* Generate user encryption key
*/
ipcMain.handle('generate-encryption-key', async (_event, userId: string) => {
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<string, string> = {
'.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 });
}
});