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:
106
electron/main.ts
106
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<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 });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user