diff --git a/config/docker/keycloak/meldestelle-realm.json b/config/docker/keycloak/meldestelle-realm.json index 4642df65..8c82d386 100644 --- a/config/docker/keycloak/meldestelle-realm.json +++ b/config/docker/keycloak/meldestelle-realm.json @@ -191,11 +191,13 @@ "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, "redirectUris": [ + "http://localhost:8080/*", "http://localhost:4000/*", "http://localhost:3000/*", "https://app.meldestelle.at/*" ], "webOrigins": [ + "http://localhost:8080", "http://localhost:4000", "http://localhost:3000", "https://app.meldestelle.at" diff --git a/frontend/core/local-db/src/jsMain/kotlin/at/mocode/frontend/core/localdb/DatabaseDriverFactory.js.kt b/frontend/core/local-db/src/jsMain/kotlin/at/mocode/frontend/core/localdb/DatabaseDriverFactory.js.kt index 77f6c04c..6425175e 100644 --- a/frontend/core/local-db/src/jsMain/kotlin/at/mocode/frontend/core/localdb/DatabaseDriverFactory.js.kt +++ b/frontend/core/local-db/src/jsMain/kotlin/at/mocode/frontend/core/localdb/DatabaseDriverFactory.js.kt @@ -8,8 +8,6 @@ import org.w3c.dom.Worker actual class DatabaseDriverFactory { actual suspend fun createDriver(): SqlDriver { // Wir nutzen eine Helper-Funktion, um den Worker zu erstellen. - // Dies ermöglicht uns, 'new URL(..., import.meta.url)' in JS zu verwenden, - // was Webpack dazu bringt, den Pfad korrekt aufzulösen. val worker = createWorker() val driver = WebWorkerDriver(worker) @@ -20,11 +18,8 @@ actual class DatabaseDriverFactory { } } -// Helper function to create the worker using proper URL resolution +// Helper function to create the worker private fun createWorker(): Worker { - return js( - """ - new Worker(new URL('sqlite.worker.js', import.meta.url), { type: 'module' }) - """ - ) + // Try the relative path again, as an absolute path might fail depending on base href + return js("new Worker('sqlite.worker.js')") } diff --git a/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js b/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js index 5b2317ed..e429b4b5 100644 --- a/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js +++ b/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js @@ -1,109 +1,122 @@ -// We do NOT import from node_modules anymore to avoid Webpack bundling issues. -// import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; - +// Minimal debug worker console.log("Worker: sqlite.worker.js loaded. Starting initialization..."); -// Minimal worker protocol compatible with SQLDelight's `web-worker-driver`. -function runWorker({driver}) { - console.log("Worker: runWorker called"); - let db = null; - const open = (name) => { - console.log("Worker: Opening database", name); - db = driver.open(name); - }; +try { + // We do NOT import from node_modules anymore to avoid Webpack bundling issues. + // import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; - // Open once with the default database name expected by SQLDelight. - open('app.db'); + // Message buffer for messages arriving before DB is ready + let messageQueue = []; + let db = null; + let isReady = false; - self.onmessage = (event) => { - const data = event.data; - try { - switch (data && data.action) { - case 'exec': { - if (!data.sql) throw new Error('exec: Missing query string'); - const rows = []; - db.exec({ - sql: data.sql, - bind: data.params ?? [], - rowMode: 'array', - callback: (row) => rows.push(row) - }); - return postMessage({id: data.id, results: {values: rows}}); + // Minimal worker protocol compatible with SQLDelight's `web-worker-driver`. + self.onmessage = (event) => { + if (!isReady) { + console.log("Worker: Buffering message (DB not ready)", event.data); + messageQueue.push(event); + return; } - case 'begin_transaction': - db.exec('BEGIN TRANSACTION;'); - return postMessage({id: data.id, results: []}); - case 'end_transaction': - db.exec('END TRANSACTION;'); - return postMessage({id: data.id, results: []}); - case 'rollback_transaction': - db.exec('ROLLBACK TRANSACTION;'); - return postMessage({id: data.id, results: []}); - default: - throw new Error(`Unsupported action: ${data && data.action}`); - } - } catch (err) { - console.error("Worker: Error processing message", err); - return postMessage({id: data && data.id, error: err?.message ?? String(err)}); - } - }; -} + processMessage(event); + }; -self.onerror = function (event) { - console.error("Error in Web Worker (onerror):", event.message, event.filename, event.lineno); - self.postMessage({type: 'error', message: event.message, filename: event.filename, lineno: event.lineno}); -}; - -async function init() { - try { - // 1. Load the sqlite3.js library manually via importScripts. - // This file is copied to the root by Webpack (CopyWebpackPlugin). - // This bypasses Webpack's module resolution for the library itself. - console.log("Worker: Loading sqlite3.js via importScripts..."); - importScripts('sqlite3.js'); - - // After importScripts, `sqlite3InitModule` should be available globally. - if (typeof self.sqlite3InitModule !== 'function') { - throw new Error("sqlite3InitModule is not defined after importScripts. Check if sqlite3.js was loaded correctly."); - } - - console.log("Worker: Fetching sqlite3.wasm manually..."); - const response = await fetch('sqlite3.wasm'); - if (!response.ok) { - throw new Error(`Failed to fetch sqlite3.wasm: ${response.status} ${response.statusText}`); - } - const wasmBinary = await response.arrayBuffer(); - console.log("Worker: sqlite3.wasm fetched successfully, size:", wasmBinary.byteLength); - - console.log("Worker: Calling sqlite3InitModule with wasmBinary..."); - const sqlite3 = await self.sqlite3InitModule({ - print: console.log, - printErr: console.error, - wasmBinary: wasmBinary - }); - - console.log("Worker: sqlite3InitModule resolved successfully"); - const opfsAvailable = 'opfs' in sqlite3; - console.log("Worker: OPFS available:", opfsAvailable); - - runWorker({ - driver: { - open: (name) => { - if (opfsAvailable) { - console.log("Initialisiere persistente OPFS Datenbank: " + name); - return new sqlite3.oo1.OpfsDb(name); - } else { - console.warn("OPFS nicht verfügbar, Fallback auf In-Memory"); - return new sqlite3.oo1.DB(name); - } + function processMessage(event) { + const data = event.data; + try { + switch (data && data.action) { + case 'exec': { + if (!data.sql) throw new Error('exec: Missing query string'); + const rows = []; + db.exec({ + sql: data.sql, + bind: data.params ?? [], + rowMode: 'array', + callback: (row) => rows.push(row) + }); + return postMessage({ id: data.id, results: { values: rows } }); + } + case 'begin_transaction': + db.exec('BEGIN TRANSACTION;'); + return postMessage({ id: data.id, results: [] }); + case 'end_transaction': + db.exec('END TRANSACTION;'); + return postMessage({ id: data.id, results: [] }); + case 'rollback_transaction': + db.exec('ROLLBACK TRANSACTION;'); + return postMessage({ id: data.id, results: [] }); + default: + throw new Error(`Unsupported action: ${data && data.action}`); + } + } catch (err) { + console.error("Worker: Error processing message", err); + return postMessage({ id: data && data.id, error: err?.message ?? String(err) }); } - } - }); + } - } catch (e) { - console.error("Database initialization error in worker:", e); - self.postMessage({type: 'error', message: 'Database initialization failed: ' + e.message}); - } + self.onerror = function(event) { + console.error("Error in Web Worker (onerror):", event.message, event.filename, event.lineno); + // Don't postMessage here as it might confuse the driver if it expects a response to a query + }; + + async function init() { + try { + // 1. Load the sqlite3.js library manually via importScripts. + console.log("Worker: Loading sqlite3.js via importScripts..."); + try { + importScripts('sqlite3.js'); + } catch (e) { + throw new Error("Failed to importScripts('sqlite3.js'). Check if file exists at root. Error: " + e.message); + } + + if (typeof self.sqlite3InitModule !== 'function') { + throw new Error("sqlite3InitModule is not defined after importScripts. Check if sqlite3.js was loaded correctly."); + } + + console.log("Worker: Fetching sqlite3.wasm manually..."); + const response = await fetch('sqlite3.wasm'); + if (!response.ok) { + throw new Error(`Failed to fetch sqlite3.wasm: ${response.status} ${response.statusText}`); + } + const wasmBinary = await response.arrayBuffer(); + console.log("Worker: sqlite3.wasm fetched successfully, size:", wasmBinary.byteLength); + + console.log("Worker: Calling sqlite3InitModule with wasmBinary..."); + const sqlite3 = await self.sqlite3InitModule({ + print: console.log, + printErr: console.error, + wasmBinary: wasmBinary + }); + + console.log("Worker: sqlite3InitModule resolved successfully"); + const opfsAvailable = 'opfs' in sqlite3; + console.log("Worker: OPFS available:", opfsAvailable); + + // Initialize DB + const dbName = 'app.db'; + if (opfsAvailable) { + console.log("Initialisiere persistente OPFS Datenbank: " + dbName); + db = new sqlite3.oo1.OpfsDb(dbName); + } else { + console.warn("OPFS nicht verfügbar, Fallback auf In-Memory"); + db = new sqlite3.oo1.DB(dbName); + } + + // Mark as ready and process queue + isReady = true; + console.log("Worker: DB Ready. Processing " + messageQueue.length + " buffered messages."); + while (messageQueue.length > 0) { + processMessage(messageQueue.shift()); + } + + } catch (e) { + console.error("Database initialization error in worker:", e); + // We can't easily communicate this back to the driver during init, + // but console.error should show up. + } + } + + init(); + +} catch (e) { + console.error("Critical Worker Error:", e); } - -init(); diff --git a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt index 10ed39a1..ffec26a6 100644 --- a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt +++ b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt @@ -41,11 +41,14 @@ val networkModule = module { // 2. API Client (Configured for Gateway & Auth Header) single(named("apiClient")) { + // Resolve TokenProvider lazily to avoid circular dependency issues during init val tokenProvider: TokenProvider? = try { get() } catch (_: Throwable) { + println("[apiClient] Warning: No TokenProvider found in Koin") null } + HttpClient { // JSON (kotlinx) configuration install(ContentNegotiation) { @@ -75,16 +78,10 @@ val networkModule = module { exponentialDelay() } - // Manual Auth Header Injection (instead of Auth plugin) to support immediate logout - install(DefaultRequest) { + // Base URL configuration + defaultRequest { val base = NetworkConfig.baseUrl.trimEnd('/') - url(base) // Set base URL - - // Inject Authorization header if token is present - val token = tokenProvider?.getAccessToken() - if (token != null) { - header("Authorization", "Bearer $token") - } + url(base) } // Logging for development @@ -96,6 +93,22 @@ val networkModule = module { } level = LogLevel.INFO } + }.also { client -> + // Dynamic Auth Header Injection via HttpSend plugin + // This ensures we get the CURRENT token for each request + if (tokenProvider != null) { + client.plugin(HttpSend).intercept { request -> + try { + val token = tokenProvider.getAccessToken() + if (token != null) { + request.header("Authorization", "Bearer $token") + } + } catch (e: Exception) { + println("[apiClient] Error getting access token: $e") + } + execute(request) + } + } } } } diff --git a/frontend/shells/meldestelle-portal/build.gradle.kts b/frontend/shells/meldestelle-portal/build.gradle.kts index 60712b0a..5de63b7c 100644 --- a/frontend/shells/meldestelle-portal/build.gradle.kts +++ b/frontend/shells/meldestelle-portal/build.gradle.kts @@ -109,34 +109,6 @@ kotlin { } } -// --------------------------------------------------------------------------- -// SQLDelight WebWorker (OPFS) resource -// --------------------------------------------------------------------------- -// `:frontend:core:local-db` ships `sqlite.worker.js` as a JS resource. -// We need to ensure this worker file is available in the output directory so the browser can load it. -// The WASM file itself is handled by Webpack (via CopyWebpackPlugin in webpack.config.d/sqlite-config.js). - -val copySqliteWorkerToWebpackSource by tasks.registering(Copy::class) { - val localDb = project(":frontend:core:local-db") - dependsOn(localDb.tasks.named("jsProcessResources")) - - from(localDb.layout.buildDirectory.file("processedResources/js/main/sqlite.worker.js")) - - // Root build directory where Kotlin JS packages are assembled. - // This is one of the directories served by webpack-dev-server for static content. - into(rootProject.layout.buildDirectory.dir("js/packages/${rootProject.name}-frontend-shells-meldestelle-portal/kotlin")) -} - -// Ensure the worker is present for the development bundle. -tasks.named("jsBrowserDevelopmentWebpack") { - dependsOn(copySqliteWorkerToWebpackSource) -} - -// Ensure the worker is present for the production bundle. -tasks.named("jsBrowserProductionWebpack") { - dependsOn(copySqliteWorkerToWebpackSource) -} - // KMP Compile-Optionen tasks.withType { compilerOptions { diff --git a/frontend/shells/meldestelle-portal/src/jsMain/resources/sqlite.worker.js b/frontend/shells/meldestelle-portal/src/jsMain/resources/sqlite.worker.js new file mode 100644 index 00000000..e429b4b5 --- /dev/null +++ b/frontend/shells/meldestelle-portal/src/jsMain/resources/sqlite.worker.js @@ -0,0 +1,122 @@ +// Minimal debug worker +console.log("Worker: sqlite.worker.js loaded. Starting initialization..."); + +try { + // We do NOT import from node_modules anymore to avoid Webpack bundling issues. + // import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; + + // Message buffer for messages arriving before DB is ready + let messageQueue = []; + let db = null; + let isReady = false; + + // Minimal worker protocol compatible with SQLDelight's `web-worker-driver`. + self.onmessage = (event) => { + if (!isReady) { + console.log("Worker: Buffering message (DB not ready)", event.data); + messageQueue.push(event); + return; + } + processMessage(event); + }; + + function processMessage(event) { + const data = event.data; + try { + switch (data && data.action) { + case 'exec': { + if (!data.sql) throw new Error('exec: Missing query string'); + const rows = []; + db.exec({ + sql: data.sql, + bind: data.params ?? [], + rowMode: 'array', + callback: (row) => rows.push(row) + }); + return postMessage({ id: data.id, results: { values: rows } }); + } + case 'begin_transaction': + db.exec('BEGIN TRANSACTION;'); + return postMessage({ id: data.id, results: [] }); + case 'end_transaction': + db.exec('END TRANSACTION;'); + return postMessage({ id: data.id, results: [] }); + case 'rollback_transaction': + db.exec('ROLLBACK TRANSACTION;'); + return postMessage({ id: data.id, results: [] }); + default: + throw new Error(`Unsupported action: ${data && data.action}`); + } + } catch (err) { + console.error("Worker: Error processing message", err); + return postMessage({ id: data && data.id, error: err?.message ?? String(err) }); + } + } + + self.onerror = function(event) { + console.error("Error in Web Worker (onerror):", event.message, event.filename, event.lineno); + // Don't postMessage here as it might confuse the driver if it expects a response to a query + }; + + async function init() { + try { + // 1. Load the sqlite3.js library manually via importScripts. + console.log("Worker: Loading sqlite3.js via importScripts..."); + try { + importScripts('sqlite3.js'); + } catch (e) { + throw new Error("Failed to importScripts('sqlite3.js'). Check if file exists at root. Error: " + e.message); + } + + if (typeof self.sqlite3InitModule !== 'function') { + throw new Error("sqlite3InitModule is not defined after importScripts. Check if sqlite3.js was loaded correctly."); + } + + console.log("Worker: Fetching sqlite3.wasm manually..."); + const response = await fetch('sqlite3.wasm'); + if (!response.ok) { + throw new Error(`Failed to fetch sqlite3.wasm: ${response.status} ${response.statusText}`); + } + const wasmBinary = await response.arrayBuffer(); + console.log("Worker: sqlite3.wasm fetched successfully, size:", wasmBinary.byteLength); + + console.log("Worker: Calling sqlite3InitModule with wasmBinary..."); + const sqlite3 = await self.sqlite3InitModule({ + print: console.log, + printErr: console.error, + wasmBinary: wasmBinary + }); + + console.log("Worker: sqlite3InitModule resolved successfully"); + const opfsAvailable = 'opfs' in sqlite3; + console.log("Worker: OPFS available:", opfsAvailable); + + // Initialize DB + const dbName = 'app.db'; + if (opfsAvailable) { + console.log("Initialisiere persistente OPFS Datenbank: " + dbName); + db = new sqlite3.oo1.OpfsDb(dbName); + } else { + console.warn("OPFS nicht verfügbar, Fallback auf In-Memory"); + db = new sqlite3.oo1.DB(dbName); + } + + // Mark as ready and process queue + isReady = true; + console.log("Worker: DB Ready. Processing " + messageQueue.length + " buffered messages."); + while (messageQueue.length > 0) { + processMessage(messageQueue.shift()); + } + + } catch (e) { + console.error("Database initialization error in worker:", e); + // We can't easily communicate this back to the driver during init, + // but console.error should show up. + } + } + + init(); + +} catch (e) { + console.error("Critical Worker Error:", e); +} diff --git a/frontend/shells/meldestelle-portal/webpack.config.d/sqlite-config.js b/frontend/shells/meldestelle-portal/webpack.config.d/sqlite-config.js index a3d1a5e2..e9477fdf 100644 --- a/frontend/shells/meldestelle-portal/webpack.config.d/sqlite-config.js +++ b/frontend/shells/meldestelle-portal/webpack.config.d/sqlite-config.js @@ -4,6 +4,9 @@ const webpack = require('webpack'); const path = require('path'); const fs = require('fs'); +console.log("SQLite Config: Current working directory (cwd):", process.cwd()); +console.log("SQLite Config: __dirname:", __dirname); + config.resolve = config.resolve || {}; config.resolve.fallback = config.resolve.fallback || {}; config.resolve.alias = config.resolve.alias || {}; @@ -16,40 +19,82 @@ config.resolve.fallback.crypto = false; // 2. Resolve sqlite3 paths let sqliteBaseDir; try { - const packagePath = path.dirname(require.resolve('@sqlite.org/sqlite-wasm/package.json')); - sqliteBaseDir = path.join(packagePath, 'sqlite-wasm/jswasm'); + const packagePath = path.dirname(require.resolve('@sqlite.org/sqlite-wasm/package.json')); + sqliteBaseDir = path.join(packagePath, 'sqlite-wasm/jswasm'); } catch (e) { - console.warn("Could not resolve @sqlite.org/sqlite-wasm path automatically. Using fallback path."); - sqliteBaseDir = path.resolve(__dirname, '../../../../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm'); + console.warn("Could not resolve @sqlite.org/sqlite-wasm path automatically. Using fallback path."); + sqliteBaseDir = path.resolve(__dirname, '../../../../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm'); } // 3. Copy ALL sqlite3 assets (wasm, js, and auxiliary workers) +const copyPatterns = []; + if (fs.existsSync(sqliteBaseDir)) { - console.log("Copying sqlite3 assets from:", sqliteBaseDir); - config.plugins.push( - new CopyWebpackPlugin({ - patterns: [ - { - from: sqliteBaseDir, - to: '.', // Copy to root of dist - globOptions: { - ignore: ['**/package.json'] // Don't copy package.json if present - }, - noErrorOnMissing: true - } - ] - }) - ); + console.log("Copying sqlite3 assets from:", sqliteBaseDir); + copyPatterns.push({ + from: sqliteBaseDir, + to: '.', // Copy to root of dist + globOptions: { + ignore: ['**/package.json'] + }, + noErrorOnMissing: true + }); } else { - console.error("ERROR: sqlite3 base directory does not exist:", sqliteBaseDir); + console.error("ERROR: sqlite3 base directory does not exist:", sqliteBaseDir); } -// 4. Alias sqlite3.wasm (still needed for some internal checks maybe) +// 4. Copy sqlite.worker.js from source +// Try multiple strategies to find the file + +// Strategy A: Relative to __dirname (webpack.config.d) +// ../../../core/local-db/src/jsMain/resources/sqlite.worker.js +const pathA = path.resolve(__dirname, '../../../core/local-db/src/jsMain/resources/sqlite.worker.js'); + +// Strategy B: Relative to process.cwd() (project root usually) +// ../../core/local-db/src/jsMain/resources/sqlite.worker.js (assuming cwd is meldestelle-portal) +const pathB = path.resolve(process.cwd(), '../../core/local-db/src/jsMain/resources/sqlite.worker.js'); + +// Strategy C: Hardcoded fallback based on typical structure +const pathC = path.resolve(__dirname, '../../../../core/local-db/src/jsMain/resources/sqlite.worker.js'); + +let workerSourcePath = null; + +if (fs.existsSync(pathA)) { + workerSourcePath = pathA; + console.log("Found sqlite.worker.js at (Strategy A):", pathA); +} else if (fs.existsSync(pathB)) { + workerSourcePath = pathB; + console.log("Found sqlite.worker.js at (Strategy B):", pathB); +} else if (fs.existsSync(pathC)) { + workerSourcePath = pathC; + console.log("Found sqlite.worker.js at (Strategy C):", pathC); +} else { + console.error("ERROR: Could not find sqlite.worker.js in any expected location!"); + console.error("Checked A:", pathA); + console.error("Checked B:", pathB); + console.error("Checked C:", pathC); +} + +if (workerSourcePath) { + copyPatterns.push({ + from: workerSourcePath, + to: 'sqlite.worker.js', + noErrorOnMissing: true + }); +} + +config.plugins.push( + new CopyWebpackPlugin({ + patterns: copyPatterns + }) +); + +// 5. Alias sqlite3.wasm (still needed for some internal checks maybe) const sqliteWasmPath = path.join(sqliteBaseDir, 'sqlite3.wasm'); config.resolve.alias['sqlite3.wasm'] = sqliteWasmPath; config.resolve.alias['./sqlite3.wasm'] = sqliteWasmPath; -// 5. Handle .wasm files +// 6. Handle .wasm files config.experiments = config.experiments || {}; config.experiments.asyncWebAssembly = true; @@ -58,36 +103,36 @@ config.module.rules = config.module.rules || []; // Treat Skiko WASM as resource to avoid parsing errors config.module.rules.push({ - test: /skiko\.wasm$/, - type: 'asset/resource' + test: /skiko\.wasm$/, + type: 'asset/resource' }); // Treat other WASM as async (default) config.module.rules.push({ - test: /\.wasm$/, - exclude: /skiko\.wasm$/, - type: 'webassembly/async' + test: /\.wasm$/, + exclude: /skiko\.wasm$/, + type: 'webassembly/async' }); -// 6. Ignore warnings +// 7. Ignore warnings config.ignoreWarnings = config.ignoreWarnings || []; config.ignoreWarnings.push(/Critical dependency: the request of a dependency is an expression/); -// 7. Fix for "webpackEmptyContext" in sqlite3.mjs +// 8. Fix for "webpackEmptyContext" in sqlite3.mjs config.plugins.push( - new webpack.ContextReplacementPlugin( - /@sqlite\.org\/sqlite-wasm/, - (data) => { - delete data.dependencies; - return data; - } - ) + new webpack.ContextReplacementPlugin( + /@sqlite\.org\/sqlite-wasm/, + (data) => { + delete data.dependencies; + return data; + } + ) ); -// 8. MIME types +// 9. MIME types config.devServer = config.devServer || {}; config.devServer.devMiddleware = config.devServer.devMiddleware || {}; config.devServer.devMiddleware.mimeTypes = { - 'application/wasm': ['wasm'], - 'application/javascript': ['js'] + 'application/wasm': ['wasm'], + 'application/javascript': ['js'] };