diff --git a/components/ScribeFooterBar.tsx b/components/ScribeFooterBar.tsx index 777926d..8c7011d 100644 --- a/components/ScribeFooterBar.tsx +++ b/components/ScribeFooterBar.tsx @@ -3,12 +3,13 @@ import {EditorContext} from "@/context/EditorContext"; import {useContext, useEffect, useState} from "react"; import {Editor} from "@tiptap/react"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faBook, faChartSimple, faHeart, faSheetPlastic} from "@fortawesome/free-solid-svg-icons"; +import {faBook, faChartSimple, faHeart, faSheetPlastic, faHardDrive} from "@fortawesome/free-solid-svg-icons"; import {SessionContext} from "@/context/SessionContext"; import {useTranslations} from "next-intl"; import {AlertContext} from "@/context/AlertContext"; import {BookContext} from "@/context/BookContext"; import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; export default function ScribeFooterBar() { const t = useTranslations(); @@ -16,6 +17,7 @@ export default function ScribeFooterBar() { const {book} = useContext(BookContext); const editor: Editor | null = useContext(EditorContext).editor; const {errorMessage} = useContext(AlertContext) + const {offlineMode} = useContext(OfflineContext) const {serverOnlyBooks,localOnlyBooks} = useContext(BooksSyncContext); const [wordsCount, setWordsCount] = useState(0); @@ -48,6 +50,12 @@ export default function ScribeFooterBar() { } } + useEffect(() => { + console.log(localOnlyBooks.length > 0 || offlineMode.isOffline); + console.log(localOnlyBooks.length); + console.log(offlineMode.isOffline); + }, []); + return (
@@ -88,12 +96,20 @@ export default function ScribeFooterBar() {
) : (
-
- - {t('scribeFooterBar.books')}: - {(serverOnlyBooks.length+localOnlyBooks.length)} -
+ { + !offlineMode.isOffline &&
+ + {serverOnlyBooks.length} +
+ } + {(localOnlyBooks.length > 0 || offlineMode.isOffline) && ( +
+ + {localOnlyBooks.length} +
+ )}
) } diff --git a/components/book/AddNewBookForm.tsx b/components/book/AddNewBookForm.tsx index ce84685..5e5135d 100644 --- a/components/book/AddNewBookForm.tsx +++ b/components/book/AddNewBookForm.tsx @@ -162,6 +162,7 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch< bookId: bookId, ...bookData }; + console.log(isCurrentlyOffline()) if (isCurrentlyOffline()){ setLocalOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => [...prevBooks, { id: book.bookId, diff --git a/components/book/BookList.tsx b/components/book/BookList.tsx index 90bd1f3..96101f1 100644 --- a/components/book/BookList.tsx +++ b/components/book/BookList.tsx @@ -99,6 +99,7 @@ export default function BookList() { session.isConnected, accessToken, offlineMode.isDatabaseInitialized, + offlineMode.isOffline, booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, @@ -350,9 +351,8 @@ export default function BookList() { ) : (
-
- +
+

{t("bookList.welcomeWritingWorkshop")}

diff --git a/components/book/settings/DeleteBook.tsx b/components/book/settings/DeleteBook.tsx index 8e21495..11c8c17 100644 --- a/components/book/settings/DeleteBook.tsx +++ b/components/book/settings/DeleteBook.tsx @@ -3,7 +3,6 @@ import {faTrash} from "@fortawesome/free-solid-svg-icons"; import {useContext, useState} from "react"; import System from "@/lib/models/System"; import {SessionContext} from "@/context/SessionContext"; -import {BookProps} from "@/lib/models/Book"; import {LangContext, LangContextProps} from "@/context/LangContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import AlertBox from "@/components/AlertBox"; @@ -43,6 +42,9 @@ export default function DeleteBook({bookId}: DeleteBookProps) { id: bookId, }); } else { + response = await window.electron.invoke('db:book:delete', { + id: bookId, + }); response = await System.authDeleteToServer( `book/delete`, { diff --git a/electron/main.ts b/electron/main.ts index 4adef21..f473d67 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -64,6 +64,13 @@ function createLoginWindow(): void { contextIsolation: true, nodeIntegration: false, sandbox: true, + webSecurity: true, + allowRunningInsecureContent: false, + experimentalFeatures: false, + enableBlinkFeatures: '', + disableBlinkFeatures: '', + webviewTag: false, + navigateOnDragDrop: false, }, frame: true, show: false, @@ -84,6 +91,25 @@ function createLoginWindow(): void { loginWindow.on('closed', () => { loginWindow = null; }); + + // Security: Block navigation to external domains + loginWindow.webContents.on('will-navigate', (event, navigationUrl) => { + const parsedUrl = new URL(navigationUrl); + if (isDev) { + if (!parsedUrl.origin.startsWith('http://localhost')) { + event.preventDefault(); + } + } else { + if (parsedUrl.protocol !== 'scribedesktop:') { + event.preventDefault(); + } + } + }); + + // Security: Block new window creation + loginWindow.webContents.setWindowOpenHandler(() => { + return { action: 'deny' }; + }); } function createMainWindow(): void { @@ -97,6 +123,13 @@ function createMainWindow(): void { contextIsolation: true, nodeIntegration: false, sandbox: true, + webSecurity: true, + allowRunningInsecureContent: false, + experimentalFeatures: false, + enableBlinkFeatures: '', + disableBlinkFeatures: '', + webviewTag: false, + navigateOnDragDrop: false, }, show: false, }); @@ -116,11 +149,43 @@ function createMainWindow(): void { mainWindow.on('closed', () => { mainWindow = null; }); + + // Security: Block navigation to external domains + mainWindow.webContents.on('will-navigate', (event, navigationUrl) => { + const parsedUrl = new URL(navigationUrl); + if (isDev) { + if (!parsedUrl.origin.startsWith('http://localhost')) { + event.preventDefault(); + } + } else { + if (parsedUrl.protocol !== 'scribedesktop:') { + event.preventDefault(); + } + } + }); + + // Security: Block new window creation + mainWindow.webContents.setWindowOpenHandler(() => { + return { action: 'deny' }; + }); } // IPC Handler pour ouvrir des liens externes (navigateur/app native) ipcMain.handle('open-external', async (_event, url: string) => { - await shell.openExternal(url); + // Security: Validate URL before opening + try { + const parsedUrl = new URL(url); + const allowedProtocols = ['http:', 'https:', 'mailto:']; + + if (!allowedProtocols.includes(parsedUrl.protocol)) { + console.error('[Security] Blocked external URL with invalid protocol:', parsedUrl.protocol); + return; + } + + await shell.openExternal(url); + } catch (error) { + console.error('[Security] Invalid URL rejected:', url); + } }); // IPC Handlers pour la gestion du token (OS-encrypted storage) @@ -347,8 +412,39 @@ ipcMain.handle('db-initialize', (_event, userId: string, encryptionKey: string) }); app.whenReady().then(():void => { - // Menu minimal pour garder les raccourcis DevTools + // Security: Disable web cache in production + if (!isDev) { + app.commandLine.appendSwitch('disable-http-cache'); + } + + // Security: Set permissions request handler + app.on('web-contents-created', (_event, contents) => { + // Allow only clipboard permissions, block others + contents.session.setPermissionRequestHandler((_webContents, permission, callback) => { + const allowedPermissions: string[] = ['clipboard-read', 'clipboard-sanitized-write']; + callback(allowedPermissions.includes(permission)); + }); + + // Block all web requests to file:// protocol + contents.session.protocol.interceptFileProtocol('file', (request, callback) => { + callback({ error: -3 }); // net::ERR_ABORTED + }); + }); + + // Menu minimal pour garder les raccourcis DevTools et clipboard const template: Electron.MenuItemConstructorOptions[] = [ + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'selectAll' } + ] + }, { label: 'View', submenu: [ @@ -364,10 +460,20 @@ app.whenReady().then(():void => { const outPath:string = path.join(process.resourcesPath, 'app.asar.unpacked/out'); protocol.handle('scribedesktop', async (request) => { + // Security: Validate and sanitize file path let filePath:string = request.url.replace('scribedesktop://', '').replace(/^\.\//, ''); + + // Security: Block path traversal attempts + if (filePath.includes('..') || filePath.includes('~')) { + console.error('[Security] Path traversal attempt blocked:', filePath); + return new Response('Forbidden', { status: 403 }); + } + const fullPath:string = path.normalize(path.join(outPath, filePath)); - + + // Security: Ensure path is within allowed directory if (!fullPath.startsWith(outPath)) { + console.error('[Security] Path escape attempt blocked:', fullPath); return new Response('Forbidden', { status: 403 }); } @@ -389,7 +495,10 @@ app.whenReady().then(():void => { }; return new Response(data, { - headers: { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' } + headers: { + 'Content-Type': mimeTypes[ext] || 'application/octet-stream', + 'X-Content-Type-Options': 'nosniff' + } }); } catch (error) { return new Response('Not found', { status: 404 });