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:
94
app/page.tsx
94
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<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 && (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
2
electron.d.ts
vendored
@@ -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>;
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
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