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 {SessionProps} from "@/lib/models/Session";
import User, {UserProps} from "@/lib/models/User"; import User, {UserProps} from "@/lib/models/User";
import {BookProps} from "@/lib/models/Book"; import {BookProps} from "@/lib/models/Book";
// Removed Next.js router imports for Electron
import ScribeTopBar from "@/components/ScribeTopBar"; import ScribeTopBar from "@/components/ScribeTopBar";
import ScribeControllerBar from "@/components/ScribeControllerBar"; import ScribeControllerBar from "@/components/ScribeControllerBar";
import ScribeLeftBar from "@/components/leftbar/ScribeLeftBar"; 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 TermsOfUse from "@/components/TermsOfUse";
import frMessages from '@/lib/locales/fr.json'; import frMessages from '@/lib/locales/fr.json';
import enMessages from '@/lib/locales/en.json'; import enMessages from '@/lib/locales/en.json';
// Removed Next.js Image import for Electron
import {NextIntlClientProvider, useTranslations} from "next-intl"; import {NextIntlClientProvider, useTranslations} from "next-intl";
import {LangContext} from "@/context/LangContext"; import {LangContext} from "@/context/LangContext";
import {AIUsageContext} from "@/context/AIUsageContext"; import {AIUsageContext} from "@/context/AIUsageContext";
@@ -45,7 +43,7 @@ function ScribeContent() {
const t = useTranslations(); const t = useTranslations();
const {lang: locale} = useContext(LangContext); const {lang: locale} = useContext(LangContext);
const {errorMessage} = useContext(AlertContext); const {errorMessage} = useContext(AlertContext);
const {initializeDatabase} = useContext(OfflineContext); const {initializeDatabase, setOfflineMode, isCurrentlyOffline} = useContext(OfflineContext);
const editor: Editor | null = useEditor({ const editor: Editor | null = useEditor({
extensions: [ extensions: [
StarterKit, StarterKit,
@@ -58,7 +56,6 @@ function ScribeContent() {
immediatelyRender: false, immediatelyRender: false,
}); });
// Router removed for Electron - using window.location instead
const [session, setSession] = useState<SessionProps>({user: null, accessToken: '', isConnected: false}); const [session, setSession] = useState<SessionProps>({user: null, accessToken: '', isConnected: false});
const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined); const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined);
const [currentBook, setCurrentBook] = useState<BookProps | null>(null); const [currentBook, setCurrentBook] = useState<BookProps | null>(null);
@@ -68,8 +65,6 @@ function ScribeContent() {
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [sessionAttempts, setSessionAttempts] = useState<number>(0)
const [isTermsAccepted, setIsTermsAccepted] = useState<boolean>(false); const [isTermsAccepted, setIsTermsAccepted] = useState<boolean>(false);
const [homeStepsGuide, setHomeStepsGuide] = useState<boolean>(false); const [homeStepsGuide, setHomeStepsGuide] = useState<boolean>(false);
const [showPinSetup, setShowPinSetup] = useState<boolean>(false); const [showPinSetup, setShowPinSetup] = useState<boolean>(false);
@@ -151,12 +146,7 @@ function ScribeContent() {
setIsTermsAccepted(session.user?.termsAccepted ?? false); setIsTermsAccepted(session.user?.termsAccepted ?? false);
setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic')); setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic'));
setIsLoading(false); setIsLoading(false);
} else {
if (sessionAttempts > 2) {
// Redirect handled by checkAuthentification
} }
}
setSessionAttempts(sessionAttempts + 1);
}, [session]); }, [session]);
useEffect((): void => { useEffect((): void => {
@@ -166,54 +156,50 @@ function ScribeContent() {
}, [currentBook]); }, [currentBook]);
// Check for PIN setup after successful connection // Check for PIN setup after successful connection
useEffect(() => { useEffect(():void => {
async function checkPinSetup() { async function checkPinSetup() {
if (session.isConnected && window.electron) { if (session.isConnected && window.electron) {
try { try {
const offlineStatus = await window.electron.offlineModeGet(); const offlineStatus = await window.electron.offlineModeGet();
console.log('[Page] Session connected, offline status:', offlineStatus);
if (!offlineStatus.hasPin) { if (!offlineStatus.hasPin) {
console.log('[Page] No PIN configured, will show setup dialog'); setTimeout(():void => {
// Show PIN setup dialog after a short delay
setTimeout(() => {
console.log('[Page] Showing PIN setup dialog'); console.log('[Page] Showing PIN setup dialog');
setShowPinSetup(true); 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(); checkPinSetup().then();
}, [session.isConnected]); // Run when session connection status changes }, [session.isConnected]);
async function handlePinVerifySuccess(userId: string): Promise<void> { async function handlePinVerifySuccess(userId: string): Promise<void> {
console.log('[OfflinePin] PIN verified successfully for user:', userId); console.log('[OfflinePin] PIN verified successfully for user:', userId);
try { try {
// Initialize database with user's encryption key
if (window.electron) { if (window.electron) {
const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId); const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId);
if (encryptionKey) { if (encryptionKey) {
await window.electron.dbInitialize(userId, encryptionKey); await window.electron.dbInitialize(userId, encryptionKey);
// Load user from local DB const localUser:UserProps = await window.electron.invoke('db:user:info');
const localUser = await window.electron.invoke('db:user:info'); if (localUser && localUser.id) {
if (localUser && localUser.success) {
// Use local data and continue in offline mode
setSession({ setSession({
isConnected: true, isConnected: true,
user: localUser.data, user: localUser,
accessToken: 'offline', // Special offline token accessToken: 'offline', // Special offline token
}); });
setShowPinVerify(false); setShowPinVerify(false);
setCurrentCredits(localUser.data.creditsBalance || 0); setCurrentCredits(localUser.creditsBalance || 0);
setAmountSpent(localUser.data.aiUsage || 0); setAmountSpent(localUser.aiUsage || 0);
console.log('[OfflinePin] Running in offline mode');
} else { } else {
errorMessage(t("homePage.errors.localDataError")); errorMessage(t("homePage.errors.localDataError"));
if (window.electron) { if (window.electron) {
@@ -282,29 +268,18 @@ function ScribeContent() {
return; return;
} }
console.log('user: ' , user);
// Initialize user in Electron (sets userId and creates/gets encryption key)
if (window.electron && user.id) { if (window.electron && user.id) {
try { try {
const initResult = await window.electron.initUser(user.id); const initResult = await window.electron.initUser(user.id);
if (!initResult.success) { if (!initResult.success) {
console.error('[Page] Failed to initialize user:', initResult.error); console.error('[Page] Failed to initialize user:', initResult.error);
} else { } else {
console.log('[Page] User initialized successfully, key created:', initResult.keyCreated);
// Check if PIN is configured for offline mode
try { try {
const offlineStatus = await window.electron.offlineModeGet(); const offlineStatus = await window.electron.offlineModeGet();
console.log('[Page] Offline status:', offlineStatus);
if (!offlineStatus.hasPin) { if (!offlineStatus.hasPin) {
// First login or no PIN configured yet setTimeout(():void => {
// 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');
setShowPinSetup(true); setShowPinSetup(true);
}, 2000); // 2 seconds delay after successful login }, 2000);
} }
} catch (error) { } catch (error) {
console.error('[Page] Error checking offline mode:', error); console.error('[Page] Error checking offline mode:', error);
@@ -321,15 +296,10 @@ function ScribeContent() {
}); });
setCurrentCredits(user.creditsBalance) setCurrentCredits(user.creditsBalance)
setAmountSpent(user.aiUsage) setAmountSpent(user.aiUsage)
// Initialiser la DB locale en Electron
if (window.electron && user.id) { if (window.electron && user.id) {
try { try {
const dbInitialized = await initializeDatabase(user.id); const dbInitialized:boolean = await initializeDatabase(user.id);
if (dbInitialized) { if (dbInitialized) {
console.log('Database initialized successfully');
// Sync user to local DB (only if not exists)
try { try {
await window.electron.invoke('db:user:sync', { await window.electron.invoke('db:user:sync', {
userId: user.id, userId: user.id,
@@ -341,7 +311,6 @@ function ScribeContent() {
console.log('User synced to local DB'); console.log('User synced to local DB');
} catch (syncError) { } catch (syncError) {
console.error('Failed to sync user to local DB:', syncError); console.error('Failed to sync user to local DB:', syncError);
// Non-blocking error, continue anyway
} }
} }
} catch (error) { } catch (error) {
@@ -349,13 +318,12 @@ function ScribeContent() {
} }
} }
} catch (e: unknown) { } catch (e: unknown) {
console.log('[Auth] Server error, checking offline mode...');
if (window.electron) { if (window.electron) {
try { try {
const offlineStatus = await window.electron.offlineModeGet(); const offlineStatus = await window.electron.offlineModeGet();
if (offlineStatus.hasPin && offlineStatus.lastUserId) { 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); setShowPinVerify(true);
setIsLoading(false); setIsLoading(false);
return; return;
@@ -370,7 +338,6 @@ function ScribeContent() {
} }
} }
// If not in offline mode or no PIN configured, show error and logout
if (e instanceof Error) { if (e instanceof Error) {
errorMessage(e.message); errorMessage(e.message);
} else { } else {
@@ -383,7 +350,7 @@ function ScribeContent() {
const offlineStatus = await window.electron.offlineModeGet(); const offlineStatus = await window.electron.offlineModeGet();
if (offlineStatus.hasPin && offlineStatus.lastUserId) { 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); setShowPinVerify(true);
setIsLoading(false); setIsLoading(false);
return; return;
@@ -428,7 +395,12 @@ function ScribeContent() {
async function getLastChapter(): Promise<void> { async function getLastChapter(): Promise<void> {
if (session?.accessToken) { if (session?.accessToken) {
try { 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) { if (response) {
setCurrentChapter(response) setCurrentChapter(response)
} else { } else {
@@ -489,12 +461,12 @@ function ScribeContent() {
</EditorContext.Provider> </EditorContext.Provider>
</div> </div>
{ {
homeStepsGuide && homeStepsGuide && !isCurrentlyOffline() &&
<GuideTour stepId={0} steps={homeSteps} onComplete={handleHomeTour} <GuideTour stepId={0} steps={homeSteps} onComplete={handleHomeTour}
onClose={(): void => setHomeStepsGuide(false)}/> onClose={(): void => setHomeStepsGuide(false)}/>
} }
{ {
!isTermsAccepted && <TermsOfUse onAccept={handleTermsAcceptance}/> !isTermsAccepted && !isCurrentlyOffline() && <TermsOfUse onAccept={handleTermsAcceptance}/>
} }
{ {
showPinSetup && window.electron && ( showPinSetup && window.electron && (

View File

@@ -17,6 +17,7 @@ import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext"; import {LangContext, LangContextProps} from "@/context/LangContext";
import CreditCounter from "@/components/CreditMeters"; import CreditCounter from "@/components/CreditMeters";
import QuillSense from "@/lib/models/QuillSense"; import QuillSense from "@/lib/models/QuillSense";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
export default function ScribeControllerBar() { export default function ScribeControllerBar() {
const {chapter, setChapter} = useContext(ChapterContext); const {chapter, setChapter} = useContext(ChapterContext);
@@ -24,12 +25,13 @@ export default function ScribeControllerBar() {
const {errorMessage} = useContext(AlertContext) const {errorMessage} = useContext(AlertContext)
const {session} = useContext(SessionContext); const {session} = useContext(SessionContext);
const t = useTranslations(); 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 isGPTEnabled: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
const isGemini: boolean = QuillSense.isOpenAIEnabled(session); const isGemini: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
const isAnthropic: boolean = QuillSense.isOpenAIEnabled(session); const isAnthropic: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
const isSubTierTwo: boolean = QuillSense.getSubLevel(session) >= 2; const isSubTierTwo: boolean = !isCurrentlyOffline() && QuillSense.getSubLevel(session) >= 2;
const hasAccess: boolean = (isGPTEnabled || isAnthropic || isGemini) || isSubTierTwo; const hasAccess: boolean = (isGPTEnabled || isAnthropic || isGemini) || isSubTierTwo;
const [showSettingPanel, setShowSettingPanel] = useState<boolean>(false); const [showSettingPanel, setShowSettingPanel] = useState<boolean>(false);

View File

@@ -146,14 +146,15 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
publicationDate: publicationDate, publicationDate: publicationDate,
desiredWordCount: wordCount, desiredWordCount: wordCount,
}, token, lang); }, token, lang);
if (!bookId) {
throw new Error(t('addNewBookForm.error.addingBook'));
}
} else { } else {
// Offline - call local database
bookId = await window.electron.invoke<string>('db:book:create', bookData); bookId = await window.electron.invoke<string>('db:book:create', bookData);
} }
if (!bookId) {
errorMessage(t('addNewBookForm.error.addingBook'))
return;
}
const book: BookProps = { const book: BookProps = {
bookId: bookId, bookId: bookId,
...bookData ...bookData

View File

@@ -172,12 +172,14 @@ export default function BookList() {
async function getBook(bookId: string): Promise<void> { async function getBook(bookId: string): Promise<void> {
try { try {
const bookResponse: BookListProps = await System.authGetQueryToServer<BookListProps>( let bookResponse: BookListProps|null = null;
`book/basic-information`, if (isCurrentlyOffline()){
accessToken, bookResponse = await window.electron.invoke('db:book:bookBasicInformation', bookId)
lang, } else {
{id: bookId} bookResponse = await System.authGetQueryToServer<BookListProps>(`book/basic-information`, accessToken, lang, {
); id: bookId
});
}
if (!bookResponse) { if (!bookResponse) {
errorMessage(t("bookList.errorBookDetails")); errorMessage(t("bookList.errorBookDetails"));
return; return;

View File

@@ -398,7 +398,6 @@ export default function TextEditor() {
const parsedContent = JSON.parse(chapter.chapterContent.content); const parsedContent = JSON.parse(chapter.chapterContent.content);
editor.commands.setContent(parsedContent); editor.commands.setContent(parsedContent);
} catch (error) { } catch (error) {
console.error('Erreur lors du parsing du contenu:', error);
editor.commands.setContent({ editor.commands.setContent({
type: "doc", type: "doc",
content: [{type: "paragraph", content: []}] content: [{type: "paragraph", content: []}]

2
electron.d.ts vendored
View File

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

View File

@@ -259,7 +259,7 @@ export default class BookRepo {
let result:RunResult let result:RunResult
try { try {
const db: Database = System.getDb(); 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) { } catch (err: unknown) {
if (err instanceof Error) { if (err instanceof Error) {
console.error(`DB Error: ${err.message}`); 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 // GET /book/:id - Get single book
ipcMain.handle('db:book:bookBasicInformation', createHandler<string, BookProps>( ipcMain.handle('db:book:bookBasicInformation', createHandler<string, BookProps>(
async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise<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 { createHandler } from '../database/LocalSystem.js';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { getSecureStorage } from '../storage/SecureStorage.js'; import { getSecureStorage } from '../storage/SecureStorage.js';
import { getDatabaseService } from '../database/database.service.js';
interface SetPinData { interface SetPinData {
pin: string; pin: string;
@@ -62,6 +63,17 @@ ipcMain.handle('offline:pin:verify', async (_event, data: VerifyPinData) => {
if (isValid) { if (isValid) {
// Set userId for session // Set userId for session
storage.set('userId', lastUserId); 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'); console.log('[Offline] PIN verified, user authenticated locally');
return { return {
success: true, success: true,

View File

@@ -1,11 +1,10 @@
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 path from 'path';
import * as url from 'url';
import {fileURLToPath} from 'url'; import {fileURLToPath} from 'url';
import * as fs from 'fs'; import * as fs from 'fs';
import { getDatabaseService } from './database/database.service.js'; import {DatabaseService, getDatabaseService} from './database/database.service.js';
import { getSecureStorage } from './storage/SecureStorage.js'; import SecureStorage, {getSecureStorage} from './storage/SecureStorage.js';
import { getUserEncryptionKey, setUserEncryptionKey, hasUserEncryptionKey } from './database/keyManager.js'; import {getUserEncryptionKey, hasUserEncryptionKey, setUserEncryptionKey} from './database/keyManager.js';
import {generateUserEncryptionKey} from './database/encryption.js'; import {generateUserEncryptionKey} from './database/encryption.js';
// Import IPC handlers // Import IPC handlers
@@ -58,7 +57,6 @@ function createLoginWindow(): void {
width: 500, width: 500,
height: 900, height: 900,
resizable: false, resizable: false,
// Ne pas définir icon sur macOS - utilise l'icône de l'app bundle
...(process.platform !== 'darwin' && { icon: iconPath }), ...(process.platform !== 'darwin' && { icon: iconPath }),
webPreferences: { webPreferences: {
preload: preloadPath, preload: preloadPath,
@@ -91,7 +89,6 @@ function createMainWindow(): void {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1200, width: 1200,
height: 800, height: 800,
// Ne pas définir icon sur macOS - utilise l'icône de l'app bundle
...(process.platform !== 'darwin' && { icon: iconPath }), ...(process.platform !== 'darwin' && { icon: iconPath }),
webPreferences: { webPreferences: {
preload: preloadPath, preload: preloadPath,
@@ -121,11 +118,8 @@ function createMainWindow(): void {
// IPC Handlers pour la gestion du token (OS-encrypted storage) // IPC Handlers pour la gestion du token (OS-encrypted storage)
ipcMain.handle('get-token', () => { ipcMain.handle('get-token', () => {
const storage = getSecureStorage(); const storage:SecureStorage = getSecureStorage();
const token = storage.get('authToken', null); return storage.get('authToken', null);
console.log('[GetToken] Token requested, exists:', !!token);
console.log('[GetToken] Storage has authToken:', storage.has('authToken'));
return token;
}); });
ipcMain.handle('set-token', (_event, token: string) => { 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 // IPC Handler pour initialiser l'utilisateur après récupération depuis le serveur
ipcMain.handle('init-user', async (_event, userId: string) => { ipcMain.handle('init-user', async (_event, userId: string) => {
console.log('[InitUser] Initializing user:', userId); const storage:SecureStorage = getSecureStorage();
const storage = getSecureStorage();
storage.set('userId', userId); storage.set('userId', userId);
storage.set('lastUserId', userId); // Save for offline mode storage.set('lastUserId', userId);
try { try {
let encryptionKey: string | null = null; let encryptionKey: string | null = null;
@@ -166,22 +158,16 @@ ipcMain.handle('init-user', async (_event, userId: string) => {
if (!hasUserEncryptionKey(userId)) { if (!hasUserEncryptionKey(userId)) {
encryptionKey = generateUserEncryptionKey(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) { if (!encryptionKey) {
console.error('[InitUser] CRITICAL: Generated key is undefined, blocking operation');
throw new Error('Failed to generate encryption key'); throw new Error('Failed to generate encryption key');
} }
setUserEncryptionKey(userId, encryptionKey); setUserEncryptionKey(userId, encryptionKey);
// Verify the key was saved const savedKey:string = getUserEncryptionKey(userId);
const savedKey = getUserEncryptionKey(userId);
console.log('[InitUser] Key verification after save:', savedKey ? `${savedKey.substring(0, 10)}...` : 'UNDEFINED'); console.log('[InitUser] Key verification after save:', savedKey ? `${savedKey.substring(0, 10)}...` : 'UNDEFINED');
if (!savedKey) { if (!savedKey) {
console.error('[InitUser] CRITICAL: Key was not saved correctly, blocking operation');
throw new Error('Failed to save encryption key'); throw new Error('Failed to save encryption key');
} }
} else { } else {
@@ -196,8 +182,6 @@ ipcMain.handle('init-user', async (_event, userId: string) => {
} }
} }
// Save userId and lastUserId to disk now that we have everything
// This is the ONLY additional save after login
if (safeStorage.isEncryptionAvailable()) { if (safeStorage.isEncryptionAvailable()) {
storage.save(); storage.save();
console.log('[InitUser] User ID and lastUserId saved to disk (encrypted)'); console.log('[InitUser] User ID and lastUserId saved to disk (encrypted)');
@@ -219,8 +203,6 @@ ipcMain.on('login-success', async (_event, token: string) => {
console.log('[Login] Received token, setting in storage'); console.log('[Login] Received token, setting in storage');
const storage = getSecureStorage(); const storage = getSecureStorage();
storage.set('authToken', token); 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) { if (loginWindow) {
loginWindow.close(); loginWindow.close();
@@ -228,19 +210,14 @@ ipcMain.on('login-success', async (_event, token: string) => {
createMainWindow(); createMainWindow();
// Save AFTER mainWindow is created (fixes macOS safeStorage issue)
setTimeout(async () => { setTimeout(async () => {
try { try {
if (safeStorage.isEncryptionAvailable()) { if (safeStorage.isEncryptionAvailable()) {
storage.save(); storage.save();
console.log('[Login] Auth token saved to disk (encrypted)');
} else { } else {
console.error('[Login] Encryption still not available after window creation');
// Try one more time after another delay
setTimeout(() => { setTimeout(() => {
if (safeStorage.isEncryptionAvailable()) { if (safeStorage.isEncryptionAvailable()) {
storage.save(); storage.save();
console.log('[Login] Auth token saved to disk (encrypted) - second attempt');
} else { } else {
console.error('[Login] CRITICAL: Cannot encrypt credentials'); console.error('[Login] CRITICAL: Cannot encrypt credentials');
} }
@@ -252,31 +229,21 @@ ipcMain.on('login-success', async (_event, token: string) => {
}, 500); }, 500);
}); });
ipcMain.on('logout', () => { ipcMain.on('logout', ():void => {
try { try {
const storage = getSecureStorage(); const storage:SecureStorage = 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'));
storage.delete('authToken'); storage.delete('authToken');
storage.delete('userId'); storage.delete('userId');
storage.delete('userLang'); 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(); storage.save();
console.log('[Logout] Cleared auth data from disk');
} catch (error) { } catch (error) {
console.error('[Logout] Error clearing storage:', error); console.error('[Logout] Error clearing storage:', error);
} }
try { try {
const db = getDatabaseService(); const db:DatabaseService = getDatabaseService();
db.close(); db.close();
} catch (error) { } catch (error) {
console.error('[Logout] Error closing database:', 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 { default: UserRepo } = await import('./database/repositories/user.repository.js');
const lang: 'fr' | 'en' = 'fr'; const lang: 'fr' | 'en' = 'fr';
// Check if user already exists in local DB
try { try {
UserRepo.fetchUserInfos(data.userId, lang); 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; return true;
} catch (error) { } catch (error) {
// User doesn't exist, create it
console.log(`[DB] User ${data.userId} not found, creating in local DB`);
await User.addUser( await User.addUser(
data.userId, data.userId,
data.firstName, data.firstName,
@@ -326,7 +286,6 @@ ipcMain.handle('db:user:sync', async (_event, data: SyncUserData): Promise<boole
'', '',
lang lang
); );
console.log(`[DB] User ${data.userId} synced successfully`);
return true; return true;
} }
} catch (error) { } catch (error) {
@@ -335,14 +294,12 @@ ipcMain.handle('db:user:sync', async (_event, data: SyncUserData): Promise<boole
} }
}); });
// ========== DATABASE IPC HANDLERS ==========
/** /**
* Generate user encryption key * Generate user encryption key
*/ */
ipcMain.handle('generate-encryption-key', async (_event, userId: string) => { ipcMain.handle('generate-encryption-key', async (_event, userId: string) => {
try { try {
const key = generateUserEncryptionKey(userId); const key:string = generateUserEncryptionKey(userId);
return { success: true, key }; return { success: true, key };
} catch (error) { } catch (error) {
console.error('Failed to generate encryption key:', 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) * Get or generate user encryption key (OS-encrypted storage)
*/ */
ipcMain.handle('get-user-encryption-key', (_event, userId: string) => { ipcMain.handle('get-user-encryption-key', (_event, userId: string) => {
const storage = getSecureStorage(); const storage:SecureStorage = getSecureStorage();
const key = storage.get(`encryptionKey-${userId}`, null); return storage.get(`encryptionKey-${userId}`, null);
return key;
}); });
/** /**
* Store user encryption key (OS-encrypted storage) * Store user encryption key (OS-encrypted storage)
*/ */
ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey: string) => { ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey: string) => {
const storage = getSecureStorage(); const storage:SecureStorage = getSecureStorage();
storage.set(`encryptionKey-${userId}`, encryptionKey); storage.set(`encryptionKey-${userId}`, encryptionKey);
return true; 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) => { ipcMain.handle('db-initialize', (_event, userId: string, encryptionKey: string) => {
try { try {
const db = getDatabaseService(); const db:DatabaseService = getDatabaseService();
db.initialize(userId, encryptionKey); db.initialize(userId, encryptionKey);
return { success: true }; return { success: true };
} catch (error) { } 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(() => { app.whenReady().then(() => {
// Enregistrer le protocole custom app:// pour servir les fichiers depuis out/
if (!isDev) { if (!isDev) {
const outPath = path.join(process.resourcesPath, 'app.asar.unpacked/out'); const outPath = path.join(process.resourcesPath, 'app.asar.unpacked/out');
protocol.handle('app', async (request) => { protocol.handle('app', async (request) => {
// Enlever app:// et ./ let filePath:string = request.url.replace('app://', '').replace(/^\.\//, '');
let filePath = request.url.replace('app://', '').replace(/^\.\//, ''); const fullPath:string = path.normalize(path.join(outPath, filePath));
const fullPath = path.normalize(path.join(outPath, filePath));
// Vérifier que le chemin est bien dans out/ (sécurité)
if (!fullPath.startsWith(outPath)) { if (!fullPath.startsWith(outPath)) {
console.error('Security: Attempted to access file outside out/:', fullPath);
return new Response('Forbidden', { status: 403 }); return new Response('Forbidden', { status: 403 });
} }
try { try {
const data = await fs.promises.readFile(fullPath); 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> = { const mimeTypes: Record<string, string> = {
'.html': 'text/html', '.html': 'text/html',
'.css': 'text/css', '.css': 'text/css',
@@ -428,7 +377,6 @@ app.whenReady().then(() => {
headers: { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' } headers: { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' }
}); });
} catch (error) { } catch (error) {
console.error('Failed to load:', fullPath, error);
return new Response('Not found', { status: 404 }); return new Response('Not found', { status: 404 });
} }
}); });