From 80f0e87d37b25d0f722c74b42005e6f26063eceb Mon Sep 17 00:00:00 2001 From: codebude Date: Mon, 7 Sep 2026 06:59:31 +0200 Subject: [PATCH 1/3] Add secure context checks to barcode scanner --- .../src/lib/components/BarcodeScanner.svelte | 22 ++++++++++++-- .../src/lib/components/BarcodeScanner.test.ts | 27 +++++++++++++++++ .../src/lib/components/ImportSearch.svelte | 12 +++++++- frontend/src/lib/i18n/locales/de.json | 6 +++- frontend/src/lib/i18n/locales/en.json | 6 +++- frontend/src/lib/i18n/locales/es.json | 6 +++- frontend/src/lib/i18n/locales/fr.json | 6 +++- frontend/src/lib/i18n/locales/zh.json | 6 +++- frontend/src/lib/utils/secureContext.test.ts | 30 +++++++++++++++++++ frontend/src/lib/utils/secureContext.ts | 6 ++++ 10 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 frontend/src/lib/utils/secureContext.test.ts create mode 100644 frontend/src/lib/utils/secureContext.ts diff --git a/frontend/src/lib/components/BarcodeScanner.svelte b/frontend/src/lib/components/BarcodeScanner.svelte index 54298112..adc55d5f 100644 --- a/frontend/src/lib/components/BarcodeScanner.svelte +++ b/frontend/src/lib/components/BarcodeScanner.svelte @@ -5,6 +5,7 @@ import { _ } from '$lib/i18n'; import { onDestroy } from 'svelte'; import { RefreshCw, X } from '@lucide/svelte'; + import { isSecureContext, SECURE_CONTEXT_DOCS_URL } from '$lib/utils/secureContext'; let { open = $bindable(false), @@ -16,6 +17,7 @@ let stream = $state(null); let scannerError = $state(null); + let notSecure = $state(false); let starting = $state(false); let detectionLocked = $state(false); let videoEl = $state(null); @@ -268,6 +270,10 @@ async function startScanner() { if (starting || stream) return; + if (!isSecureContext()) { + notSecure = true; + return; + } if (!navigator.mediaDevices?.getUserMedia) throw new Error($_('scanner.noCamera')); starting = true; scannerError = null; @@ -320,12 +326,13 @@ } $effect(() => { - if (open && !stream && !starting && !scannerError) { + if (open && !stream && !starting && !scannerError && !notSecure) { void startScanner(); return; } if (!open) { scannerError = null; + notSecure = false; if (stream) { void stopScanner(); } @@ -366,7 +373,18 @@ {/if} - {#if !scannerError} + {#if notSecure} +
+ + {$_('scanner.secureContextRequired')}{' '} + + {$_('scanner.secureContextDocsLink')} + + +
+ {/if} + + {#if !scannerError && !notSecure}
+ {:else if !secureContext} +

+ {$_('import.scanUnavailable')}{' '} + + {$_('import.scanUnavailableDocsLink')} + +

{/if} {#if stages.length > 0} diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json index e7e042b2..976299e7 100644 --- a/frontend/src/lib/i18n/locales/de.json +++ b/frontend/src/lib/i18n/locales/de.json @@ -219,6 +219,8 @@ "googleAdded": "Google-Books-Ergebnisse hinzugefügt: {count}", "scan": "Scannen", "scanIsbn": "ISBN-Barcode scannen", + "scanUnavailable": "Barcode-Scannen ist nicht verfügbar, weil die App nicht in einem sicheren Kontext bereitgestellt wird.", + "scanUnavailableDocsLink": "Mehr erfahren", "importFailed": "Import fehlgeschlagen", "searchFailed": "Suche fehlgeschlagen", "scannedIsbn": "ISBN gescannt: {isbn}", @@ -240,7 +242,9 @@ "switchCamera": "Kamera wechseln", "zoom": "Zoom", "zoomLevel": "Zoom {zoom}x", - "close": "Scanner schließen" + "close": "Scanner schließen", + "secureContextRequired": "Kamerazugriff erfordert einen sicheren Kontext (HTTPS oder localhost).", + "secureContextDocsLink": "Mehr erfahren" }, "coverPicker": { "dropzone": "Bild hierher ziehen oder", diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 94a76f0d..8de077c3 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -219,6 +219,8 @@ "googleAdded": "Google Books results added: {count}", "scan": "Scan", "scanIsbn": "Scan ISBN barcode", + "scanUnavailable": "Barcode scanning is unavailable because the app is not served in a secure context.", + "scanUnavailableDocsLink": "Learn more", "importFailed": "Import failed", "searchFailed": "Search failed", "scannedIsbn": "Scanned ISBN: {isbn}", @@ -240,7 +242,9 @@ "switchCamera": "Switch camera", "zoom": "Zoom", "zoomLevel": "Zoom {zoom}x", - "close": "Close scanner" + "close": "Close scanner", + "secureContextRequired": "Camera access requires a secure context (HTTPS or localhost).", + "secureContextDocsLink": "Learn more" }, "coverPicker": { "dropzone": "Drag & drop an image, or", diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json index cc69ab32..ae46fd13 100644 --- a/frontend/src/lib/i18n/locales/es.json +++ b/frontend/src/lib/i18n/locales/es.json @@ -219,6 +219,8 @@ "googleAdded": "Resultados de Google Books añadidos: {count}", "scan": "Escanear", "scanIsbn": "Escanear código de barras ISBN", + "scanUnavailable": "El escaneo de códigos de barras no está disponible porque la aplicación no se sirve en un contexto seguro.", + "scanUnavailableDocsLink": "Más información", "importFailed": "Importación fallida", "searchFailed": "Búsqueda fallida", "scannedIsbn": "ISBN escaneado: {isbn}", @@ -240,7 +242,9 @@ "switchCamera": "Cambiar cámara", "zoom": "Zoom", "zoomLevel": "Zoom {zoom}x", - "close": "Cerrar escáner" + "close": "Cerrar escáner", + "secureContextRequired": "El acceso a la cámara requiere un contexto seguro (HTTPS o localhost).", + "secureContextDocsLink": "Más información" }, "coverPicker": { "dropzone": "Arrastra una imagen aquí, o", diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json index 4871f99f..00004c5c 100644 --- a/frontend/src/lib/i18n/locales/fr.json +++ b/frontend/src/lib/i18n/locales/fr.json @@ -219,6 +219,8 @@ "googleAdded": "Résultats Google Books ajoutés : {count}", "scan": "Scanner", "scanIsbn": "Scanner le code-barres ISBN", + "scanUnavailable": "Le scan de code-barres est indisponible car l'application n'est pas servie dans un contexte sécurisé.", + "scanUnavailableDocsLink": "En savoir plus", "importFailed": "Échec de l'importation", "searchFailed": "Échec de la recherche", "scannedIsbn": "ISBN scanné : {isbn}", @@ -240,7 +242,9 @@ "switchCamera": "Changer de caméra", "zoom": "Zoom", "zoomLevel": "Zoom {zoom}x", - "close": "Fermer le scanner" + "close": "Fermer le scanner", + "secureContextRequired": "L'accès à la caméra nécessite un contexte sécurisé (HTTPS ou localhost).", + "secureContextDocsLink": "En savoir plus" }, "coverPicker": { "dropzone": "Glisse et dépose une image ici, ou", diff --git a/frontend/src/lib/i18n/locales/zh.json b/frontend/src/lib/i18n/locales/zh.json index 3e7affe8..afd053e0 100644 --- a/frontend/src/lib/i18n/locales/zh.json +++ b/frontend/src/lib/i18n/locales/zh.json @@ -219,6 +219,8 @@ "googleAdded": "Google Books 结果已添加:{count}", "scan": "扫描", "scanIsbn": "扫描 ISBN 条码", + "scanUnavailable": "由于应用未在安全上下文中提供,条码扫描不可用。", + "scanUnavailableDocsLink": "了解更多", "importFailed": "导入失败", "searchFailed": "搜索失败", "scannedIsbn": "已扫描 ISBN:{isbn}", @@ -240,7 +242,9 @@ "switchCamera": "切换摄像头", "zoom": "缩放", "zoomLevel": "缩放 {zoom}x", - "close": "关闭扫描器" + "close": "关闭扫描器", + "secureContextRequired": "摄像头访问需要安全上下文(HTTPS 或 localhost)。", + "secureContextDocsLink": "了解更多" }, "coverPicker": { "dropzone": "拖放图片到此处,或", diff --git a/frontend/src/lib/utils/secureContext.test.ts b/frontend/src/lib/utils/secureContext.test.ts new file mode 100644 index 00000000..111aa240 --- /dev/null +++ b/frontend/src/lib/utils/secureContext.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { isSecureContext, SECURE_CONTEXT_DOCS_URL } from '$lib/utils/secureContext'; + +describe('isSecureContext', () => { + const originalDescriptor = Object.getOwnPropertyDescriptor(window, 'isSecureContext'); + + afterEach(() => { + if (originalDescriptor) { + Object.defineProperty(window, 'isSecureContext', originalDescriptor); + } + }); + + it('returns true when window.isSecureContext is true', () => { + Object.defineProperty(window, 'isSecureContext', { configurable: true, value: true }); + expect(isSecureContext()).toBe(true); + }); + + it('returns false when window.isSecureContext is false', () => { + Object.defineProperty(window, 'isSecureContext', { configurable: true, value: false }); + expect(isSecureContext()).toBe(false); + }); +}); + +describe('SECURE_CONTEXT_DOCS_URL', () => { + it('points to the library guide section on the docs site', () => { + expect(SECURE_CONTEXT_DOCS_URL).toBe( + 'https://docs.librislog.app/guide/using-librislog/library#isbn-barcode-scan' + ); + }); +}); \ No newline at end of file diff --git a/frontend/src/lib/utils/secureContext.ts b/frontend/src/lib/utils/secureContext.ts new file mode 100644 index 00000000..799df354 --- /dev/null +++ b/frontend/src/lib/utils/secureContext.ts @@ -0,0 +1,6 @@ +export const SECURE_CONTEXT_DOCS_URL = + 'https://docs.librislog.app/guide/using-librislog/library.html#isbn-barcode-scan'; + +export function isSecureContext(): boolean { + return typeof window !== 'undefined' && window.isSecureContext === true; +} \ No newline at end of file From d7d89d2107baa7e5d96edfbba7495a283ed56fa8 Mon Sep 17 00:00:00 2001 From: codebude Date: Mon, 7 Sep 2026 06:59:53 +0200 Subject: [PATCH 2/3] Add notes on secure context and camera usage to the docs --- docs/guide/getting-started.md | 2 ++ docs/guide/using-librislog/library.md | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index a9f4c719..289c58c5 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -7,6 +7,8 @@ Get LibrisLog running in minutes. - [Docker](https://docs.docker.com/get-docker/) (includes Docker Compose) - `curl` or `wget` (to download files) +> **Camera features need a secure context**: The ISBN barcode scanner (and any camera use) only works when the app is served over **HTTPS** or via `http://localhost`. If you access the app over plain `http://` on a remote address, the camera won't start. See the [library guide](/guide/using-librislog/library#isbn-barcode-scan) for details. + ## Setup Download the files, create your environment, and generate a secure encryption key. diff --git a/docs/guide/using-librislog/library.md b/docs/guide/using-librislog/library.md index a02c2c0a..48a2084d 100644 --- a/docs/guide/using-librislog/library.md +++ b/docs/guide/using-librislog/library.md @@ -67,7 +67,15 @@ The search automatically tries Open Library first, then falls back to other sour ### ISBN Barcode Scan -On mobile devices, use the camera to scan ISBN barcodes. The app uses the device's camera with real-time barcode detection to quickly look up books. +Use the camera to scan ISBN barcodes. The app uses the device's camera with real-time barcode detection to quickly look up books. + +::: warning Requires a secure context + +Camera access is only available when LibrisLog is served in a **secure context**. A page is a secure context when it is served over **HTTPS** or from `http://localhost` (or `http://127.0.0.1`). Accessing the app via a plain `http://` address on a remote host — e.g. `http://192.168.1.10:8001` — is **not** a secure context, and the camera will not start. See [MDN: Secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Dangerous_Contexts) for details. + +If the barcode scan button is hidden or the scanner shows a black box, your browser is likely blocking camera access because the app is not running in a secure context. Serve LibrisLog behind HTTPS (a reverse proxy with a TLS certificate) or access it via `localhost` to enable scanning. + +::: ## Editing Books From 9dfac058ecf303a532a1ba4481c6a683d1e75e25 Mon Sep 17 00:00:00 2001 From: codebude Date: Mon, 7 Sep 2026 07:05:07 +0200 Subject: [PATCH 3/3] Fix wrong secure context urls in tests --- frontend/src/lib/components/BarcodeScanner.test.ts | 2 +- frontend/src/lib/utils/secureContext.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/BarcodeScanner.test.ts b/frontend/src/lib/components/BarcodeScanner.test.ts index 0a79bbdc..ac2ee0d6 100644 --- a/frontend/src/lib/components/BarcodeScanner.test.ts +++ b/frontend/src/lib/components/BarcodeScanner.test.ts @@ -356,7 +356,7 @@ describe('BarcodeScanner', () => { const link = screen.getByRole('link', { name: /learn more/i }); expect(link).toHaveAttribute( 'href', - 'https://docs.librislog.app/guide/using-librislog/library#isbn-barcode-scan' + 'https://docs.librislog.app/guide/using-librislog/library.html#isbn-barcode-scan' ); expect(getUserMedia).not.toHaveBeenCalled(); expect(screen.queryByRole('slider', { name: /zoom/i })).not.toBeInTheDocument(); diff --git a/frontend/src/lib/utils/secureContext.test.ts b/frontend/src/lib/utils/secureContext.test.ts index 111aa240..e1bd2e62 100644 --- a/frontend/src/lib/utils/secureContext.test.ts +++ b/frontend/src/lib/utils/secureContext.test.ts @@ -24,7 +24,7 @@ describe('isSecureContext', () => { describe('SECURE_CONTEXT_DOCS_URL', () => { it('points to the library guide section on the docs site', () => { expect(SECURE_CONTEXT_DOCS_URL).toBe( - 'https://docs.librislog.app/guide/using-librislog/library#isbn-barcode-scan' + 'https://docs.librislog.app/guide/using-librislog/library.html#isbn-barcode-scan' ); }); }); \ No newline at end of file