diff --git a/.env b/.env index fbc7289c..8b623a70 100644 --- a/.env +++ b/.env @@ -50,3 +50,8 @@ GATEWAY_SERVER_PORT=8081 # --- MICROSERVICES --- PING_SERVICE_PORT=8082:8082 PING_DEBUG_PORT=5006:5006 + +# --- CLIENT APPLICATIONS --- +WEB_APP_PORT=4000:4000 +DESKTOP_APP_VNC_PORT=5901:5901 +DESKTOP_APP_NOVNC_PORT=6080:6080 diff --git a/.env.template b/.env.template index 3ef34841..fde1d32b 100644 --- a/.env.template +++ b/.env.template @@ -51,3 +51,10 @@ CONSUL_PORT=8500:8500 GATEWAY_PORT=8081 # Debug Port für IntelliJ (Remote JVM Debug) GATEWAY_DEBUG_PORT=5005 + +# --- CLIENT APPLICATIONS --- +# Web-App (Kotlin/JS, kein WASM) +WEB_APP_PORT=4000:4000 +# Desktop-App (VNC/noVNC) +DESKTOP_APP_VNC_PORT=5901:5901 +DESKTOP_APP_NOVNC_PORT=6080:6080 diff --git a/clients/app/build.gradle.kts b/clients/app/build.gradle.kts index 4bd64b37..bf8828be 100644 --- a/clients/app/build.gradle.kts +++ b/clients/app/build.gradle.kts @@ -44,7 +44,6 @@ kotlin { webpackTask { mainOutputFileName = "web-app.js" - output.libraryTarget = "commonjs2" } // Development Server konfigurieren @@ -67,7 +66,10 @@ kotlin { // WASM, nur wenn explizit aktiviert if (enableWasm) { @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { browser() } + wasmJs { + browser() + binaries.executable() + } } sourceSets { @@ -76,8 +78,8 @@ kotlin { implementation(project(":clients:shared")) implementation(project(":clients:shared:common-ui")) implementation(project(":clients:shared:navigation")) + implementation(project(":clients:auth-feature")) implementation(project(":clients:ping-feature")) - implementation(project(":clients:members-feature")) // Compose Multiplatform implementation(compose.runtime) @@ -129,7 +131,9 @@ tasks.withType { jvmTarget.set(JvmTarget.JVM_21) freeCompilerArgs.addAll( "-opt-in=kotlin.RequiresOptIn", - "-Xskip-metadata-version-check" // Für bleeding-edge Versionen + "-Xskip-metadata-version-check", // Für bleeding-edge Versionen + // Suppress beta warning for expect/actual declarations used in this module + "-Xexpect-actual-classes" ) } } diff --git a/clients/app/package-lock.json b/clients/app/package-lock.json deleted file mode 100644 index c5834b8f..00000000 --- a/clients/app/package-lock.json +++ /dev/null @@ -1,3833 +0,0 @@ -{ - "name": "app", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "webpack-bundle-analyzer": "^4.10.2" - }, - "devDependencies": { - "@types/jest": "^29.2.5", - "jest": "^29.3.1" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.4" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "license": "MIT" - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/node": { - "version": "24.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", - "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.14.0" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz", - "integrity": "sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001748", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", - "integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", - "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.232", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz", - "integrity": "sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "license": "MIT", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "license": "MIT" - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "license": "(WTFPL OR MIT)", - "bin": { - "opener": "bin/opener-bin.js" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/webpack-bundle-analyzer": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", - "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "0.5.7", - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "commander": "^7.2.0", - "debounce": "^1.2.1", - "escape-string-regexp": "^4.0.0", - "gzip-size": "^6.0.0", - "html-escaper": "^2.0.2", - "opener": "^1.5.2", - "picocolors": "^1.0.0", - "sirv": "^2.0.3", - "ws": "^7.3.1" - }, - "bin": { - "webpack-bundle-analyzer": "lib/bin/analyzer.js" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/clients/app/package.json b/clients/app/package.json deleted file mode 100644 index 20fa3b2c..00000000 --- a/clients/app/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "devDependencies": { - "@types/jest": "^29.2.5", - "jest": "^29.3.1" - }, - "scripts": { - "test": "jest" - }, - "dependencies": { - "webpack-bundle-analyzer": "^4.10.2" - } -} diff --git a/clients/app/src/commonMain/kotlin/MainApp.kt b/clients/app/src/commonMain/kotlin/MainApp.kt index 552b6a88..a3573b2e 100644 --- a/clients/app/src/commonMain/kotlin/MainApp.kt +++ b/clients/app/src/commonMain/kotlin/MainApp.kt @@ -2,107 +2,258 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.foundation.layout.* import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp -import at.mocode.clients.membersfeature.ProfileScreen -import at.mocode.clients.membersfeature.ProfileViewModel +import androidx.compose.runtime.collectAsState import at.mocode.clients.shared.navigation.AppScreen +import at.mocode.clients.authfeature.AuthenticatedHttpClient +import at.mocode.clients.authfeature.AuthTokenManager +import at.mocode.clients.pingfeature.PingScreen +import at.mocode.clients.pingfeature.PingViewModel +import at.mocode.clients.shared.core.AppConstants +import androidx.compose.material3.OutlinedTextField +import androidx.compose.ui.text.input.PasswordVisualTransformation +import kotlinx.coroutines.launch +import androidx.compose.runtime.rememberCoroutineScope +import at.mocode.clients.authfeature.AuthApiClient +import at.mocode.clients.authfeature.oauth.OAuthPkceService +import at.mocode.clients.authfeature.oauth.AuthCallbackParams +import at.mocode.clients.authfeature.oauth.CallbackParams @Composable fun MainApp() { - MaterialTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - var currentScreen by remember { mutableStateOf(AppScreen.Home) } + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + var currentScreen by remember { mutableStateOf(AppScreen.Home) } - when (currentScreen) { - is AppScreen.Home -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile }) - is AppScreen.Login -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile }) - is AppScreen.Ping -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile }) - is AppScreen.Profile -> ProfileScreen(viewModel = remember { ProfileViewModel() }) + val authTokenManager = remember { AuthenticatedHttpClient.getAuthTokenManager() } + val pingViewModel = remember { PingViewModel() } + val scope = rememberCoroutineScope() + + // Handle PKCE callback on an app load (web) + LaunchedEffect(Unit) { + val callback: CallbackParams? = AuthCallbackParams.parse() + if (callback != null) { + val code = callback.code + val state = callback.state + val pkce = OAuthPkceService.current() + if (pkce != null && pkce.state == state) { + val api = AuthApiClient() + val res = api.exchangeAuthorizationCode(code, pkce.codeVerifier, AppConstants.webRedirectUri()) + val token = res.token + if (res.success && token != null) { + authTokenManager.setToken(token) + OAuthPkceService.clear() + currentScreen = AppScreen.Profile } + } } + } + + when (currentScreen) { + is AppScreen.Home -> WelcomeScreen( + authTokenManager = authTokenManager, + onOpenPing = { AppScreen.Ping }, + onOpenLogin = { + // Fallback to the local LoginScreen (Password Grant) if PKCE cannot be started + currentScreen = AppScreen.Login + }, + onOpenProfile = { currentScreen = AppScreen.Profile } + ) + + is AppScreen.Login -> LoginScreen( + authTokenManager = authTokenManager, + onLoginSuccess = { currentScreen = AppScreen.Profile } + ) + + is AppScreen.Ping -> PingScreen(viewModel = pingViewModel) + is AppScreen.Profile -> AuthStatusScreen( + authTokenManager = authTokenManager, + onBackToHome = { currentScreen = AppScreen.Home } + ) + + else -> {} + } } + } } @Composable -fun DevelopmentScreen(onOpenProfile: () -> Unit) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - "🚀 Meldestelle Development Mode", - style = MaterialTheme.typography.headlineMedium - ) +private fun WelcomeScreen( + authTokenManager: AuthTokenManager, + onOpenPing: () -> Unit, + onOpenLogin: () -> Unit, + onOpenProfile: () -> Unit +) { + val authState by authTokenManager.authState.collectAsState() + val uriHandler = LocalUriHandler.current + val scope = rememberCoroutineScope() - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - "🌐 Backend Connectivity", - style = MaterialTheme.typography.titleMedium - ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "Willkommen zur Meldestelle", + style = MaterialTheme.typography.headlineMedium + ) - var testStatus by remember { mutableStateOf("Not tested") } - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button(onClick = { testStatus = "Testing Gateway..." }) { - Text("Test Gateway") - } - Button(onClick = { testStatus = "Testing Ping Service..." }) { - Text("Test Ping Service") - } - Button(onClick = onOpenProfile) { - Text("Open Profile") - } - } - - Text("Status: $testStatus") - } - } - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - "🏓 Ping Service Tests", - style = MaterialTheme.typography.titleMedium - ) - - var isDarkMode by remember { mutableStateOf(false) } - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button(onClick = { /* TODO: Health Check */ }) { - Text("Health Check") - } - Button(onClick = { /* TODO: Ping Normal */ }) { - Text("Ping Normal") - } - Button(onClick = { isDarkMode = !isDarkMode }) { - Text("Toggle Dark Mode") - } - } - - Text("Dark Mode: ${if(isDarkMode) "🌙 Enabled" else "☀️ Disabled"}") - } - } - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - "✅ System Status", - style = MaterialTheme.typography.titleMedium - ) - Text("Frontend: 🟢 Running") - Text("Backend: ⚠️ Testing needed") - Text("Build: ✅ Successful") - } + // Auth info + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + if (authState.isAuthenticated) { + Text("Du bist als ${authState.username ?: authState.userId ?: "unbekannt"} angemeldet.") + Spacer(Modifier.height(8.dp)) + Button(onClick = onOpenProfile) { Text("Profil anzeigen") } + } else { + Text("Du bist nicht angemeldet.") } + } } + + // Actions + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Button(onClick = onOpenPing, modifier = Modifier.weight(1f)) { Text("Ping-Service") } + if (!authState.isAuthenticated) { + Button( + onClick = { + // Try PKCE login (Authorization Code Flow w/ PKCE) + scope.launch { + try { + val pkce = OAuthPkceService.startAuth() + val url = OAuthPkceService.buildAuthorizeUrl(pkce, AppConstants.webRedirectUri()) + uriHandler.openUri(url) + } catch (_: Throwable) { + // Fallback: open the local Login screen (Password Grant) + onOpenLogin() + } + } + }, + modifier = Modifier.weight(1f) + ) { Text("Login") } + } + } + + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedButton( + onClick = { uriHandler.openUri(AppConstants.registerUrl()) }, + modifier = Modifier.weight(1f) + ) { Text("Registrieren (Keycloak)") } + + OutlinedButton( + onClick = { uriHandler.openUri(AppConstants.loginUrl()) }, + modifier = Modifier.weight(1f) + ) { Text("Keycloak Login-Seite") } + } + + // Desktop Download Link + OutlinedButton( + onClick = { uriHandler.openUri(AppConstants.desktopDownloadUrl()) }, + modifier = Modifier.fillMaxWidth() + ) { Text("Desktop-App herunterladen") } + } +} + +@Composable +private fun AuthStatusScreen( + authTokenManager: AuthTokenManager, + onBackToHome: () -> Unit +) { + val authState by authTokenManager.authState.collectAsState() + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text("Profil / Status", style = MaterialTheme.typography.headlineMedium) + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + if (authState.isAuthenticated) { + Text("Du bist als ${authState.username ?: authState.userId ?: "unbekannt"} angemeldet.") + Spacer(Modifier.height(8.dp)) + Button(onClick = { + authTokenManager.clearToken() + onBackToHome() + }) { Text("Abmelden") } + } else { + Text("Nicht angemeldet.") + Spacer(Modifier.height(8.dp)) + Button(onClick = onBackToHome) { Text("Zurück zur Startseite") } + } + } + } + } +} + +@Composable +private fun LoginScreen( + authTokenManager: AuthTokenManager, + onLoginSuccess: () -> Unit +) { + var username by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var error by remember { mutableStateOf(null) } + var isLoading by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val api = remember { AuthApiClient() } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text("Anmeldung", style = MaterialTheme.typography.headlineMedium) + + OutlinedTextField( + value = username, + onValueChange = { username = it }, + label = { Text("Benutzername") }, + singleLine = true, + enabled = !isLoading, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Passwort") }, + singleLine = true, + enabled = !isLoading, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth() + ) + + error?.let { + Text(it, color = MaterialTheme.colorScheme.error) + } + + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Button( + onClick = { + error = null + isLoading = true + scope.launch { + val res = api.login(username.trim(), password) + val token = res.token + if (res.success && token != null) { + authTokenManager.setToken(token) + isLoading = false + onLoginSuccess() + } else { + isLoading = false + error = res.message ?: "Login fehlgeschlagen" + } + } + }, + enabled = !isLoading && username.isNotBlank() && password.isNotBlank() + ) { Text(if (isLoading) "Bitte warten…" else "Login") } + } + } } diff --git a/clients/app/src/commonMain/kotlin/at/mocode/clients/app/App.kt.backup b/clients/app/src/commonMain/kotlin/at/mocode/clients/app/App.kt.backup deleted file mode 100644 index ed5cfd60..00000000 --- a/clients/app/src/commonMain/kotlin/at/mocode/clients/app/App.kt.backup +++ /dev/null @@ -1,65 +0,0 @@ -package at.mocode.clients.app - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import at.mocode.clients.shared.commonui.components.AppHeader -import at.mocode.clients.shared.commonui.components.AppScaffold -import at.mocode.clients.shared.commonui.theme.AppTheme -import at.mocode.clients.shared.navigation.AppScreen -import at.mocode.clients.pingfeature.PingScreen -import at.mocode.clients.pingfeature.PingViewModel -import at.mocode.clients.authfeature.LoginScreen -import at.mocode.clients.authfeature.AuthTokenManager -import androidx.compose.runtime.collectAsState - -@Composable -fun App() { - var currentScreen: AppScreen by remember { mutableStateOf(AppScreen.Home) } - // Create a single PingViewModel instance for the lifetime of the App composition. - val pingViewModel: PingViewModel = remember { PingViewModel() } - // Create a single AuthTokenManager instance for the lifetime of the App composition. - val authTokenManager: AuthTokenManager = remember { AuthTokenManager() } - // Observe authentication state - val authState by authTokenManager.authState.collectAsState() - - AppTheme { - AppScaffold( - header = { - AppHeader( - title = "Meldestelle", - onNavigateToPing = { currentScreen = AppScreen.Ping }, - onNavigateToLogin = { currentScreen = AppScreen.Login }, - onLogout = { - authTokenManager.clearToken() - currentScreen = AppScreen.Home - }, - isAuthenticated = authState.isAuthenticated, - username = authState.username, - userPermissions = authState.permissions.map { it.name } - ) - }, - { paddingValues -> - Box(modifier = Modifier.padding(paddingValues)) { - when (currentScreen) { - is AppScreen.Home -> { - LandingScreen(authTokenManager = authTokenManager) - } - - is AppScreen.Login -> { - LoginScreen( - authTokenManager = authTokenManager, - onLoginSuccess = { currentScreen = AppScreen.Home } - ) - } - - is AppScreen.Ping -> { - PingScreen(viewModel = pingViewModel) - } - } - } - } - ) - } -} diff --git a/clients/app/src/commonMain/kotlin/at/mocode/clients/app/LandingScreen.kt.backup b/clients/app/src/commonMain/kotlin/at/mocode/clients/app/LandingScreen.kt.backup deleted file mode 100644 index 035a6e29..00000000 --- a/clients/app/src/commonMain/kotlin/at/mocode/clients/app/LandingScreen.kt.backup +++ /dev/null @@ -1,232 +0,0 @@ -package at.mocode.clients.app - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import at.mocode.clients.authfeature.AuthTokenManager -import at.mocode.clients.authfeature.Permission - -@Composable -fun LandingScreen( - authTokenManager: AuthTokenManager? = null -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top - ) { - Text( - text = "Willkommen bei Meldestelle", - style = MaterialTheme.typography.headlineLarge, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.primary - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column( - modifier = Modifier.padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Eine moderne, skalierbare Frontend-Architektur", - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center, - fontWeight = FontWeight.Medium - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "Diese Anwendung demonstriert eine \"Shell + Feature-Module\"-Architektur " + - "basierend auf Kotlin Multiplatform. Sie spiegelt die DDD-Struktur des Backends " + - "wider und ist als native Desktop-Anwendung (JVM) und Web-Anwendung (JS/Wasm) lauffähig.", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2 - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Text( - text = "🚀 Technologien:", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - TechItem("Kotlin Multiplatform") - TechItem("Jetpack Compose Multiplatform") - TechItem("Material Design 3") - TechItem("Ktor Client") - TechItem("Domain-Driven Design") - } - } - } - - Spacer(modifier = Modifier.height(24.dp)) - - Text( - text = "Verwenden Sie das Ping Service Menü oben, um die API-Funktionalität zu testen.", - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - // Permission-based UI demonstration - authTokenManager?.let { tokenManager -> - val authState by tokenManager.authState.collectAsState() - - if (authState.isAuthenticated && authState.permissions.isNotEmpty()) { - Spacer(modifier = Modifier.height(32.dp)) - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ) - ) { - Column( - modifier = Modifier.padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "🔐 Verfügbare Funktionen", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSecondaryContainer - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // Admin features (visible only to users with delete permissions) - if (tokenManager.isAdmin()) { - PermissionCard( - title = "👑 Administrator-Bereich", - description = "Vollzugriff auf alle System-Funktionen", - permissions = listOf("Alle Berechtigungen", "System-Verwaltung", "Benutzer-Management"), - backgroundColor = MaterialTheme.colorScheme.errorContainer, - textColor = MaterialTheme.colorScheme.onErrorContainer - ) - } - - // Management features (visible to users with create/update permissions) - if (tokenManager.canCreate() || tokenManager.canUpdate()) { - PermissionCard( - title = "✏️ Verwaltung", - description = "Erstellen und bearbeiten von Daten", - permissions = buildList { - if (tokenManager.hasPermission(Permission.PERSON_CREATE)) add("Personen erstellen") - if (tokenManager.hasPermission(Permission.PERSON_UPDATE)) add("Personen bearbeiten") - if (tokenManager.hasPermission(Permission.VEREIN_CREATE)) add("Vereine erstellen") - if (tokenManager.hasPermission(Permission.VEREIN_UPDATE)) add("Vereine bearbeiten") - if (tokenManager.hasPermission(Permission.PFERD_CREATE)) add("Pferde erstellen") - if (tokenManager.hasPermission(Permission.PFERD_UPDATE)) add("Pferde bearbeiten") - if (tokenManager.hasPermission(Permission.VERANSTALTUNG_CREATE)) add("Veranstaltungen erstellen") - if (tokenManager.hasPermission(Permission.VERANSTALTUNG_UPDATE)) add("Veranstaltungen bearbeiten") - }, - backgroundColor = MaterialTheme.colorScheme.primaryContainer, - textColor = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - - // Read-only features (visible to all authenticated users) - if (tokenManager.canRead()) { - PermissionCard( - title = "👁️ Ansicht", - description = "Nur-Lese-Zugriff auf Daten", - permissions = buildList { - if (tokenManager.hasPermission(Permission.PERSON_READ)) add("Personen anzeigen") - if (tokenManager.hasPermission(Permission.VEREIN_READ)) add("Vereine anzeigen") - if (tokenManager.hasPermission(Permission.PFERD_READ)) add("Pferde anzeigen") - if (tokenManager.hasPermission(Permission.VERANSTALTUNG_READ)) add("Veranstaltungen anzeigen") - }, - backgroundColor = MaterialTheme.colorScheme.surfaceVariant, - textColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - } - } -} - -@Composable -private fun TechItem(text: String) { - Text( - text = "• $text", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(vertical = 2.dp) - ) -} - -@Composable -private fun PermissionCard( - title: String, - description: String, - permissions: List, - backgroundColor: androidx.compose.ui.graphics.Color, - textColor: androidx.compose.ui.graphics.Color -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - colors = CardDefaults.cardColors( - containerColor = backgroundColor - ) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = textColor - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = description, - style = MaterialTheme.typography.bodyMedium, - color = textColor - ) - - if (permissions.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) - - permissions.forEach { permission -> - Text( - text = "✓ $permission", - style = MaterialTheme.typography.bodySmall, - color = textColor, - modifier = Modifier.padding(vertical = 2.dp) - ) - } - } - } - } -} diff --git a/clients/app/src/commonMain/kotlin/screens/DevelopmentScreen.kt.backup b/clients/app/src/commonMain/kotlin/screens/DevelopmentScreen.kt.backup deleted file mode 100644 index 4272c41b..00000000 --- a/clients/app/src/commonMain/kotlin/screens/DevelopmentScreen.kt.backup +++ /dev/null @@ -1,202 +0,0 @@ -package screens - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import at.mocode.clients.shared.presentation.store.AppStore -import at.mocode.clients.shared.presentation.state.AppState -import at.mocode.clients.pingfeature.PingViewModel -import at.mocode.ping.api.HealthResponse -import at.mocode.ping.api.PingResponse -import at.mocode.ping.api.EnhancedPingResponse - -@Composable -fun DevelopmentScreen(appStore: AppStore) { - Column( - modifier = Modifier.fillMaxSize().padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - "🚀 Meldestelle Development Mode", - style = MaterialTheme.typography.headlineMedium - ) - - // Backend Connectivity Tests - BackendTestSection() - - // Ping Service Test - PingTestSection() - - // State Debugging - StateDebugSection(appStore) - } -} - -@Composable -fun BackendTestSection() { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("🌐 Backend Connectivity", style = MaterialTheme.typography.titleMedium) - - var testStatus by remember { mutableStateOf("Not tested") } - var isLoading by remember { mutableStateOf(false) } - - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button( - onClick = { - // TODO: Test Gateway Connection - isLoading = true - testStatus = "Testing..." - }, - enabled = !isLoading - ) { - Text("Test Gateway") - } - - Button( - onClick = { - // TODO: Test Ping Service Direct - isLoading = true - testStatus = "Testing direct connection..." - }, - enabled = !isLoading - ) { - Text("Test Ping Service") - } - } - - if (isLoading) { - CircularProgressIndicator(modifier = Modifier.padding(8.dp)) - } - - Text("Status: $testStatus") - } - } -} - -@Composable -fun PingTestSection() { - val pingViewModel = remember { PingViewModel() } - val uiState = pingViewModel.uiState - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("🏓 Ping Service Integration", style = MaterialTheme.typography.titleMedium) - - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button( - onClick = { pingViewModel.performHealthCheck() }, - enabled = !uiState.isLoading - ) { - Text("Health Check") - } - - Button( - onClick = { pingViewModel.performSimplePing() }, - enabled = !uiState.isLoading - ) { - Text("Simple Ping") - } - - Button( - onClick = { pingViewModel.performEnhancedPing(true) }, - enabled = !uiState.isLoading - ) { - Text("Test Circuit Breaker") - } - } - - if (uiState.isLoading) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) - } - - // Results Display - uiState.healthResponse?.let { health -> - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ) - ) { - Column(modifier = Modifier.padding(8.dp)) { - Text("✅ Health Check Result:") - Text("Status: ${health.status}") - Text("Service: ${health.service}") - Text("Healthy: ${health.healthy}") - Text("Timestamp: ${health.timestamp}") - } - } - } - - uiState.simplePingResponse?.let { ping -> - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ) - ) { - Column(modifier = Modifier.padding(8.dp)) { - Text("🏓 Simple Ping Result:") - Text("Status: ${ping.status}") - Text("Service: ${ping.service}") - Text("Timestamp: ${ping.timestamp}") - } - } - } - - uiState.enhancedPingResponse?.let { ping -> - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer - ) - ) { - Column(modifier = Modifier.padding(8.dp)) { - Text("⚡ Enhanced Ping Result:") - Text("Status: ${ping.status}") - Text("Circuit Breaker: ${ping.circuitBreakerState}") - Text("Response Time: ${ping.responseTime}ms") - Text("Service: ${ping.service}") - } - } - } - - uiState.errorMessage?.let { error -> - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ) - ) { - Text( - "❌ Error: $error", - color = MaterialTheme.colorScheme.onErrorContainer, - modifier = Modifier.padding(8.dp) - ) - } - } - } - } -} - -@Composable -fun StateDebugSection(appStore: AppStore) { - val appState by appStore.state.collectAsState() - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("🔍 App State Debug", style = MaterialTheme.typography.titleMedium) - - Text("Auth State: ${if(appState.auth.isAuthenticated) "✅ Authenticated" else "❌ Not Authenticated"}") - Text("Current Route: ${appState.navigation.currentRoute}") - Text("Dark Mode: ${if(appState.ui.isDarkMode) "🌙 Enabled" else "☀️ Disabled"}") - Text("Online: ${if(appState.network.isOnline) "🟢 Online" else "🔴 Offline"}") - - Button( - onClick = { - appStore.dispatch(at.mocode.clients.shared.presentation.actions.AppAction.UI.ToggleDarkMode) - } - ) { - Text("Toggle Dark Mode") - } - } - } -} diff --git a/clients/app/src/commonTest/kotlin/ComposeAppCommonTest.kt b/clients/app/src/commonTest/kotlin/ComposeAppCommonTest.kt new file mode 100644 index 00000000..b671cfd1 --- /dev/null +++ b/clients/app/src/commonTest/kotlin/ComposeAppCommonTest.kt @@ -0,0 +1,10 @@ +import kotlin.test.Test +import kotlin.test.assertEquals + +class ComposeAppCommonTest { + + @Test + fun example() { + assertEquals(3, 1 + 2) + } +} diff --git a/clients/app/src/jsMain/kotlin/DevelopmentMode.js.kt b/clients/app/src/jsMain/kotlin/DevelopmentMode.js.kt index 75f2a318..87df6fe7 100644 --- a/clients/app/src/jsMain/kotlin/DevelopmentMode.js.kt +++ b/clients/app/src/jsMain/kotlin/DevelopmentMode.js.kt @@ -1,2 +1,2 @@ actual fun isDevelopmentMode(): Boolean = - kotlinx.browser.window.location.hostname == "localhost" + kotlinx.browser.window.location.hostname == "localhost" diff --git a/clients/app/src/jsMain/kotlin/main.kt b/clients/app/src/jsMain/kotlin/main.kt index 3be845ed..7c71a485 100644 --- a/clients/app/src/jsMain/kotlin/main.kt +++ b/clients/app/src/jsMain/kotlin/main.kt @@ -1,21 +1,37 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport import kotlinx.browser.document -import kotlinx.browser.window import org.w3c.dom.HTMLElement @OptIn(ExperimentalComposeUiApi::class) fun main() { - window.onload = { - try { - val root = document.getElementById("ComposeTarget") as HTMLElement - ComposeViewport(root) { - MainApp() - } - } catch (e: Exception) { - console.error("Failed to start Compose Web app", e) - document.getElementById("root")?.innerHTML = - "
❌ Failed to load app: ${e.message}
" - } + console.log("[WebApp] main() entered") + fun startApp() { + try { + console.log("[WebApp] startApp(): readyState=", document.asDynamic().readyState) + val root = document.getElementById("ComposeTarget") as HTMLElement + console.log("[WebApp] ComposeTarget exists? ", (root != null)) + ComposeViewport(root) { + MainApp() + } + // Remove the static loading placeholder if present + (document.querySelector(".loading") as? HTMLElement)?.let { it.parentElement?.removeChild(it) } + console.log("[WebApp] ComposeViewport mounted, loading placeholder removed") + } catch (e: Exception) { + console.error("Failed to start Compose Web app", e) + val fallbackTarget = (document.getElementById("ComposeTarget") ?: document.body) as HTMLElement + fallbackTarget.innerHTML = + "
❌ Failed to load app: ${e.message}
" } + } + + // Start immediately if DOM is already parsed, otherwise wait for DOMContentLoaded. + val state = document.asDynamic().readyState as String? + if (state == "interactive" || state == "complete") { + console.log("[WebApp] DOM already ready (", state, ") → starting immediately") + startApp() + } else { + console.log("[WebApp] Waiting for DOMContentLoaded, current state:", state) + document.addEventListener("DOMContentLoaded", { startApp() }) + } } diff --git a/clients/app/src/jsMain/resources/index.html b/clients/app/src/jsMain/resources/index.html index aa53fc16..47fb586b 100644 --- a/clients/app/src/jsMain/resources/index.html +++ b/clients/app/src/jsMain/resources/index.html @@ -1,39 +1,27 @@ - - Meldestelle - Web Development - - + + + Meldestelle - Web + + + -
- -
🚀 Loading Meldestelle...
-
- +
+
Loading...
+
+ + diff --git a/clients/app/src/jsMain/resources/styles.css b/clients/app/src/jsMain/resources/styles.css index bebe4e80..2c25ff81 100644 --- a/clients/app/src/jsMain/resources/styles.css +++ b/clients/app/src/jsMain/resources/styles.css @@ -1,12 +1,22 @@ html, body { - height: 100vh; - margin: 0; - padding: 0; - overflow: hidden; /* Verhindert Scrollbalken durch die Canvas */ + margin: 0; + padding: 0; + font-family: system-ui, -apple-system, sans-serif; + background: #fafafa; + overflow: hidden; /* Verhindert Scrollbalken durch die Canvas */ } #ComposeTarget { - height: 100vh; - display: flex; - flex-direction: column; + height: 100vh; + display: flex; + flex-direction: column; +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + font-size: 18px; + color: #666; } diff --git a/clients/app/src/jsMain/resources/sw.js b/clients/app/src/jsMain/resources/sw.js index b2a7400c..fd053936 100644 --- a/clients/app/src/jsMain/resources/sw.js +++ b/clients/app/src/jsMain/resources/sw.js @@ -58,7 +58,8 @@ self.addEventListener('fetch', (event) => { .then((resp) => { if (resp && resp.status === 200 && resp.type === 'basic') { const copy = resp.clone(); - caches.open(CACHE_NAME).then((cache) => cache.put('/index.html', copy)).catch(() => {}); + caches.open(CACHE_NAME).then((cache) => cache.put('/index.html', copy)).catch(() => { + }); } return resp; }) @@ -83,7 +84,8 @@ self.addEventListener('fetch', (event) => { .then((resp) => { if (resp && resp.status === 200 && resp.type === 'basic') { const copy = resp.clone(); - caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)).catch(() => {}); + caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)).catch(() => { + }); } return resp; }) diff --git a/clients/app/src/jvmMain/kotlin/DevelopmentMode.jvm.kt b/clients/app/src/jvmMain/kotlin/DevelopmentMode.jvm.kt index fa368369..e48d0561 100644 --- a/clients/app/src/jvmMain/kotlin/DevelopmentMode.jvm.kt +++ b/clients/app/src/jvmMain/kotlin/DevelopmentMode.jvm.kt @@ -1,2 +1,2 @@ actual fun isDevelopmentMode(): Boolean = - System.getProperty("development.mode", "false").toBoolean() + System.getProperty("development.mode", "false").toBoolean() diff --git a/clients/app/src/jvmMain/kotlin/main.kt b/clients/app/src/jvmMain/kotlin/main.kt index f5c75eda..68db9fd9 100644 --- a/clients/app/src/jvmMain/kotlin/main.kt +++ b/clients/app/src/jvmMain/kotlin/main.kt @@ -4,11 +4,11 @@ import androidx.compose.ui.window.WindowState import androidx.compose.ui.unit.dp fun main() = application { - Window( - onCloseRequest = ::exitApplication, - title = "Meldestelle - Desktop Development", - state = WindowState(width = 1200.dp, height = 800.dp) - ) { - MainApp() - } + Window( + onCloseRequest = ::exitApplication, + title = "Meldestelle - Desktop Development", + state = WindowState(width = 1200.dp, height = 800.dp) + ) { + MainApp() + } } diff --git a/clients/app/src/wasmJsMain/kotlin/main.kt b/clients/app/src/wasmJsMain/kotlin/main.kt index c2baff13..9420a2ba 100644 --- a/clients/app/src/wasmJsMain/kotlin/main.kt +++ b/clients/app/src/wasmJsMain/kotlin/main.kt @@ -1,13 +1,12 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport -import at.mocode.clients.app.App import kotlinx.browser.document import org.w3c.dom.HTMLElement @OptIn(ExperimentalComposeUiApi::class) fun main() { - val root = document.getElementById("ComposeTarget") as HTMLElement - ComposeViewport(root) { - App() - } + val root = document.getElementById("ComposeTarget") as HTMLElement + ComposeViewport(root) { + MainApp() + } } diff --git a/clients/app/webpack.config.d/webpack.config.js b/clients/app/webpack.config.d/webpack.config.js index 1d7730e3..86333e47 100644 --- a/clients/app/webpack.config.d/webpack.config.js +++ b/clients/app/webpack.config.d/webpack.config.js @@ -3,49 +3,46 @@ // Bundle-Analyse für Development (optional, only if package is available) if (process.env.ANALYZE_BUNDLE === 'true') { - try { - const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; - config.plugins.push(new BundleAnalyzerPlugin({ - analyzerMode: 'static', - openAnalyzer: false, - reportFilename: 'bundle-report.html' - })); - console.log('Bundle analyzer enabled'); - } catch (e) { - console.log('Bundle analyzer not available (webpack-bundle-analyzer not installed)'); - } + try { + const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; + config.plugins.push(new BundleAnalyzerPlugin({ + analyzerMode: 'static', + openAnalyzer: false, + reportFilename: 'bundle-report.html' + })); + console.log('Bundle analyzer enabled'); + } catch (e) { + console.log('Bundle analyzer not available (webpack-bundle-analyzer not installed)'); + } } -// Weitere Optimierungen hinzufügen (erweitert bestehende config) -config.optimization = { - ...config.optimization, // Behalte Kotlin/JS Optimierungen - splitChunks: { - chunks: 'all', - cacheGroups: { - vendor: { - test: /[\\/]node_modules[\\/]/, - name: 'vendor', - chunks: 'all' - } - } - } -}; +// Hinweis: Wir liefern eine statische index.html aus src/jsMain/resources aus. +// Diese Datei enthält nur einen Script-Tag zu "web-app.js" und wird NICHT +// vom HtmlWebpackPlugin generiert. Zusätzliche Chunks (z. B. vendor/runtime) +// würden dann nicht automatisch injiziert und führen dazu, dass die App nicht startet +// (Bildschirm bleibt auf "Loading..."). +// +// Daher überschreiben wir config.optimization NICHT mehr mit splitChunks. +// Wenn später Chunking gewünscht ist, muss die index.html durch die generierte +// HTML ersetzt oder die zusätzlichen Chunks manuell eingebunden werden. +// +// (Frühere splitChunks-Konfiguration wurde bewusst entfernt.) // Development Server Konfiguration erweitern if (config.devServer) { - config.devServer = { - ...config.devServer, - historyApiFallback: true, - hot: true, - // API Proxy für Backend-Anfragen (Array-Format für moderne Webpack) - proxy: [ - { - context: ['/api'], - target: 'http://localhost:8081', - changeOrigin: true, - secure: false, - pathRewrite: { '^/api': '' } - } - ] - } + config.devServer = { + ...config.devServer, + historyApiFallback: true, + hot: true, + // API Proxy für Backend-Anfragen (Array-Format für moderne Webpack) + proxy: [ + { + context: ['/api'], + target: 'http://localhost:8081', + changeOrigin: true, + secure: false, + pathRewrite: {'^/api': ''} + } + ] + } } diff --git a/clients/auth-feature/build.gradle.kts b/clients/auth-feature/build.gradle.kts index aeb5c050..4bd935e0 100644 --- a/clients/auth-feature/build.gradle.kts +++ b/clients/auth-feature/build.gradle.kts @@ -6,117 +6,123 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget * und den UI-Baukasten (common-ui), aber es kennt keine anderen Features. */ plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.kotlinSerialization) } group = "at.mocode.clients" version = "1.0.0" kotlin { - val enableWasm = providers.gradleProperty("enableWasm").orNull == "true" + val enableWasm = providers.gradleProperty("enableWasm").orNull == "true" - jvmToolchain(21) + jvmToolchain(21) - jvm() + jvm() + + js { + browser { + testTask { + enabled = false + } + } + binaries.executable() + } + + // WASM, nur wenn explizit aktiviert + if (enableWasm) { + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + } + + sourceSets { + commonMain.dependencies { + // UI Kit + implementation(project(":clients:shared:common-ui")) + + // Shared Konfig & Utilities (AppConfig + BuildConfig) + implementation(project(":clients:shared")) + + // Compose dependencies + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + implementation(compose.components.resources) + implementation(compose.materialIconsExtended) + + // Ktor client for HTTP calls + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.contentNegotiation) + implementation(libs.ktor.client.serialization.kotlinx.json) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.client.auth) + + // Coroutines and serialization + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + + // DateTime for multiplatform time handling + implementation(libs.kotlinx.datetime) + + // ViewModel lifecycle + implementation(libs.androidx.lifecycle.viewmodelCompose) + implementation(libs.androidx.lifecycle.runtimeCompose) - js { - browser { - testTask { - enabled = false - } - } } - // WASM, nur wenn explizit aktiviert + commonTest.dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutines.test) + implementation("io.ktor:ktor-client-mock:${libs.versions.ktor.get()}") + } + + jvmTest.dependencies { + implementation(libs.mockk) + implementation(projects.platform.platformTesting) + implementation(libs.bundles.testing.jvm) + } + + jvmMain.dependencies { + implementation(libs.ktor.client.cio) + } + + jsMain.dependencies { + implementation(libs.ktor.client.js) + implementation(libs.ktor.client.auth) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.datetime) + } + + // WASM SourceSet, nur wenn aktiviert if (enableWasm) { - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { browser() } - } - - sourceSets { - commonMain.dependencies { - // UI Kit - implementation(project(":clients:shared:common-ui")) - - // Shared Konfig & Utilities (AppConfig + BuildConfig) - implementation(project(":clients:shared")) - - // Compose dependencies - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.ui) - implementation(compose.components.resources) - implementation(compose.materialIconsExtended) - - // Ktor client for HTTP calls - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.contentNegotiation) - implementation(libs.ktor.client.serialization.kotlinx.json) - implementation(libs.ktor.client.logging) - implementation(libs.ktor.client.auth) - - // Coroutines and serialization - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.serialization.json) - - // DateTime for multiplatform time handling - implementation(libs.kotlinx.datetime) - - // ViewModel lifecycle - implementation(libs.androidx.lifecycle.viewmodelCompose) - implementation(libs.androidx.lifecycle.runtimeCompose) - - } - - commonTest.dependencies { - implementation(libs.kotlin.test) - implementation(libs.kotlinx.coroutines.test) - implementation("io.ktor:ktor-client-mock:${libs.versions.ktor.get()}") - } - - jvmTest.dependencies { - implementation(libs.mockk) - implementation(projects.platform.platformTesting) - implementation(libs.bundles.testing.jvm) - } - - jvmMain.dependencies { - implementation(libs.ktor.client.cio) - } - - jsMain.dependencies { - implementation(libs.ktor.client.js) - implementation(libs.ktor.client.auth) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.serialization.json) - implementation(libs.kotlinx.datetime) - } - - // WASM SourceSet, nur wenn aktiviert - if (enableWasm) { - val wasmJsMain = getByName("wasmJsMain") - wasmJsMain.dependencies { - implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7] - - // ✅ HINZUFÜGEN: Compose für shared UI components für WASM - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material3) - } - } + val wasmJsMain = getByName("wasmJsMain") + wasmJsMain.dependencies { + implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7] + + // ✅ HINZUFÜGEN: Compose für shared UI components für WASM + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + } } + } } // KMP Compile-Optionen tasks.withType { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - freeCompilerArgs.addAll( - "-opt-in=kotlin.RequiresOptIn" - ) - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + freeCompilerArgs.addAll( + "-opt-in=kotlin.RequiresOptIn", + // Suppress beta warning for expect/actual classes as per project decision + "-Xexpect-actual-classes" + ) + } } diff --git a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthApiClient.kt b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthApiClient.kt index 542ea650..e0158efa 100644 --- a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthApiClient.kt +++ b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthApiClient.kt @@ -1,11 +1,9 @@ package at.mocode.clients.authfeature -import at.mocode.clients.shared.AppConfig +import at.mocode.clients.shared.core.AppConstants import io.ktor.client.call.* -import io.ktor.client.request.* import io.ktor.client.request.forms.* import io.ktor.http.* -import io.ktor.http.content.* import kotlinx.serialization.Serializable /** @@ -13,138 +11,181 @@ import kotlinx.serialization.Serializable */ @Serializable data class LoginRequest( - val username: String, - val password: String + val username: String, + val password: String ) @Serializable data class LoginResponse( - val success: Boolean, - val token: String? = null, - val message: String? = null, - val userId: String? = null, - val username: String? = null + val success: Boolean, + val token: String? = null, + val message: String? = null, + val userId: String? = null, + val username: String? = null ) /** * HTTP client for authentication API calls */ class AuthApiClient( - // Keycloak Basis-URL (z. B. http://localhost:8180) - private val keycloakBaseUrl: String = AppConfig.KEYCLOAK_URL, - // Realm-Name in Keycloak - private val realm: String = AppConfig.KEYCLOAK_REALM, - // Client-ID (Public Client empfohlen für Frontend-Flows) - private val clientId: String = AppConfig.KEYCLOAK_CLIENT_ID, - // Optional: Client-Secret (nur bei vertraulichen Clients erforderlich) - private val clientSecret: String? = null + // Keycloak Basis-URL (z. B. http://localhost:8180) + private val keycloakBaseUrl: String = AppConstants.KEYCLOAK_URL, + // Realm-Name in Keycloak + private val realm: String = AppConstants.KEYCLOAK_REALM, + // Client-ID (Public Client empfohlen für Frontend-Flows) + private val clientId: String = AppConstants.KEYCLOAK_CLIENT_ID, + // Optional: Client-Secret (nur bei vertraulichen Clients erforderlich) + private val clientSecret: String? = null ) { - private val client = AuthenticatedHttpClient.createUnauthenticated() + private val client = AuthenticatedHttpClient.createUnauthenticated() - /** - * Authenticate user with username and password - */ - suspend fun login(username: String, password: String): LoginResponse { - val tokenEndpoint = "$keycloakBaseUrl/realms/$realm/protocol/openid-connect/token" - return try { - val response = client.submitForm( - url = tokenEndpoint, - formParameters = Parameters.build { - append("grant_type", "password") - append("client_id", clientId) - if (!clientSecret.isNullOrBlank()) { - append("client_secret", clientSecret) - } - append("username", username) - append("password", password) - } - ) { - // Explicit: URL-encoded Form - contentType(ContentType.Application.FormUrlEncoded) - } - - if (response.status.isSuccess()) { - val kc = response.body() - LoginResponse( - success = true, - token = kc.access_token, - message = null, - userId = null, - username = username - ) - } else { - LoginResponse( - success = false, - message = "Login fehlgeschlagen: HTTP ${response.status.value}" - ) - } - } catch (e: Exception) { - LoginResponse( - success = false, - message = "Verbindungsfehler: ${e.message}" - ) + /** + * Authenticate user with username and password + */ + suspend fun login(username: String, password: String): LoginResponse { + val tokenEndpoint = "$keycloakBaseUrl/realms/$realm/protocol/openid-connect/token" + return try { + val response = client.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "password") + append("client_id", clientId) + if (!clientSecret.isNullOrBlank()) { + append("client_secret", clientSecret) + } + append("username", username) + append("password", password) } + ) { + // Explicit: URL-encoded Form + contentType(ContentType.Application.FormUrlEncoded) + } + + if (response.status.isSuccess()) { + val kc = response.body() + LoginResponse( + success = true, + token = kc.access_token, + message = null, + userId = null, + username = username + ) + } else { + LoginResponse( + success = false, + message = "Login fehlgeschlagen: HTTP ${response.status.value}" + ) + } + } catch (e: Exception) { + LoginResponse( + success = false, + message = "Verbindungsfehler: ${e.message}" + ) } + } - /** - * Refresh authentication token - */ - suspend fun refreshToken(refreshToken: String): LoginResponse { - val tokenEndpoint = "$keycloakBaseUrl/realms/$realm/protocol/openid-connect/token" - return try { - val response = client.submitForm( - url = tokenEndpoint, - formParameters = Parameters.build { - append("grant_type", "refresh_token") - append("client_id", clientId) - if (!clientSecret.isNullOrBlank()) { - append("client_secret", clientSecret) - } - append("refresh_token", refreshToken) - } - ) { - contentType(ContentType.Application.FormUrlEncoded) - } - - if (response.status.isSuccess()) { - val kc = response.body() - LoginResponse( - success = true, - token = kc.access_token, - message = null - ) - } else { - LoginResponse( - success = false, - message = "Token refresh fehlgeschlagen: HTTP ${response.status.value}" - ) - } - } catch (e: Exception) { - LoginResponse( - success = false, - message = "Token refresh Fehler: ${e.message}" - ) + /** + * Exchange an authorization code (PKCE) for tokens + */ + suspend fun exchangeAuthorizationCode(code: String, codeVerifier: String, redirectUri: String): LoginResponse { + val tokenEndpoint = "$keycloakBaseUrl/realms/$realm/protocol/openid-connect/token" + return try { + val response = client.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "authorization_code") + append("client_id", clientId) + if (!clientSecret.isNullOrBlank()) { + append("client_secret", clientSecret) + } + append("code", code) + append("code_verifier", codeVerifier) + append("redirect_uri", redirectUri) } - } + ) { + contentType(ContentType.Application.FormUrlEncoded) + } - /** - * Logout and invalidate token - */ - suspend fun logout(token: String): Boolean { - // Empfehlung: Frontend-seitig Token lokal verwerfen. - // Optional könnten hier Keycloak-Endpoints für Token-Revocation aufgerufen werden. - return true + if (response.status.isSuccess()) { + val kc = response.body() + LoginResponse( + success = true, + token = kc.access_token, + message = null + ) + } else { + LoginResponse( + success = false, + message = "Code-Exchange fehlgeschlagen: HTTP ${'$'}{response.status.value}" + ) + } + } catch (e: Exception) { + LoginResponse( + success = false, + message = "Code-Exchange Fehler: ${'$'}{e.message}" + ) } + } - @Serializable - private data class KeycloakTokenResponse( - val access_token: String, - val expires_in: Long? = null, - val refresh_expires_in: Long? = null, - val refresh_token: String? = null, - val token_type: String? = null, - val not_before_policy: Long? = null, - val session_state: String? = null, - val scope: String? = null - ) + /** + * Refresh authentication token + */ + suspend fun refreshToken(refreshToken: String): LoginResponse { + val tokenEndpoint = "$keycloakBaseUrl/realms/$realm/protocol/openid-connect/token" + return try { + val response = client.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "refresh_token") + append("client_id", clientId) + if (!clientSecret.isNullOrBlank()) { + append("client_secret", clientSecret) + } + append("refresh_token", refreshToken) + } + ) { + contentType(ContentType.Application.FormUrlEncoded) + } + + if (response.status.isSuccess()) { + val kc = response.body() + LoginResponse( + success = true, + token = kc.access_token, + message = null + ) + } else { + LoginResponse( + success = false, + message = "Token refresh fehlgeschlagen: HTTP ${response.status.value}" + ) + } + } catch (e: Exception) { + LoginResponse( + success = false, + message = "Token refresh Fehler: ${e.message}" + ) + } + } + + /** + * Logout and invalidate token + */ + suspend fun logout(token: String): Boolean { + // Empfehlung: Frontend-seitig Token lokal verwerfen. + // Optional könnten hier Keycloak-Endpoints für Token-Revocation aufgerufen werden. + return true + } + + @Serializable + private data class KeycloakTokenResponse( + val access_token: String, + val expires_in: Long? = null, + val refresh_expires_in: Long? = null, + val refresh_token: String? = null, + val token_type: String? = null, + val not_before_policy: Long? = null, + val session_state: String? = null, + val scope: String? = null + ) } diff --git a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthTokenManager.kt b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthTokenManager.kt index d08d9606..590055a6 100644 --- a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthTokenManager.kt +++ b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthTokenManager.kt @@ -5,12 +5,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.longOrNull -import kotlinx.serialization.json.contentOrNull import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.time.ExperimentalTime @@ -20,29 +14,29 @@ import kotlin.time.ExperimentalTime */ @Serializable enum class Permission { - // Person management - PERSON_READ, - PERSON_CREATE, - PERSON_UPDATE, - PERSON_DELETE, + // Person management + PERSON_READ, + PERSON_CREATE, + PERSON_UPDATE, + PERSON_DELETE, - // Club management - VEREIN_READ, - VEREIN_CREATE, - VEREIN_UPDATE, - VEREIN_DELETE, + // Club management + VEREIN_READ, + VEREIN_CREATE, + VEREIN_UPDATE, + VEREIN_DELETE, - // Event management - VERANSTALTUNG_READ, - VERANSTALTUNG_CREATE, - VERANSTALTUNG_UPDATE, - VERANSTALTUNG_DELETE, + // Event management + VERANSTALTUNG_READ, + VERANSTALTUNG_CREATE, + VERANSTALTUNG_UPDATE, + VERANSTALTUNG_DELETE, - // Horse management - PFERD_READ, - PFERD_CREATE, - PFERD_UPDATE, - PFERD_DELETE + // Horse management + PFERD_READ, + PFERD_CREATE, + PFERD_UPDATE, + PFERD_DELETE } /** @@ -50,23 +44,23 @@ enum class Permission { */ @Serializable data class JwtPayload( - val sub: String? = null, // User ID - val username: String? = null, // Username - val exp: Long? = null, // Expiration timestamp - val iat: Long? = null, // Issued at timestamp - val iss: String? = null, // Issuer - val permissions: List? = null // Permissions array + val sub: String? = null, // User ID + val username: String? = null, // Username + val exp: Long? = null, // Expiration timestamp + val iat: Long? = null, // Issued at timestamp + val iss: String? = null, // Issuer + val permissions: List? = null // Permissions array ) /** * Authentication state */ data class AuthState( - val isAuthenticated: Boolean = false, - val token: String? = null, - val userId: String? = null, - val username: String? = null, - val permissions: List = emptyList() + val isAuthenticated: Boolean = false, + val token: String? = null, + val userId: String? = null, + val username: String? = null, + val permissions: List = emptyList() ) /** @@ -78,267 +72,267 @@ data class AuthState( */ class AuthTokenManager { - private var currentToken: String? = null - private var tokenPayload: JwtPayload? = null + private var currentToken: String? = null + private var tokenPayload: JwtPayload? = null - private val _authState = MutableStateFlow(AuthState()) - val authState: StateFlow = _authState.asStateFlow() + private val _authState = MutableStateFlow(AuthState()) + val authState: StateFlow = _authState.asStateFlow() - /** - * Store JWT token in memory - */ - fun setToken(token: String) { - currentToken = token - tokenPayload = parseJwtPayload(token) + /** + * Store JWT token in memory + */ + fun setToken(token: String) { + currentToken = token + tokenPayload = parseJwtPayload(token) - // Parse permissions from token payload - val permissions = tokenPayload?.permissions?.mapNotNull { permissionString -> - try { - Permission.valueOf(permissionString) - } catch (e: IllegalArgumentException) { - // Ignore unknown permissions - null - } - } ?: emptyList() + // Parse permissions from token payload + val permissions = tokenPayload?.permissions?.mapNotNull { permissionString -> + try { + Permission.valueOf(permissionString) + } catch (e: IllegalArgumentException) { + // Ignore unknown permissions + null + } + } ?: emptyList() - _authState.value = AuthState( - isAuthenticated = true, - token = token, - userId = tokenPayload?.sub, - username = tokenPayload?.username, - permissions = permissions - ) + _authState.value = AuthState( + isAuthenticated = true, + token = token, + userId = tokenPayload?.sub, + username = tokenPayload?.username, + permissions = permissions + ) + } + + /** + * Get current JWT token + */ + fun getToken(): String? = currentToken + + /** + * Check if we have a valid (non-expired) token + */ + @OptIn(ExperimentalTime::class) + fun hasValidToken(): Boolean { + val token = currentToken ?: return false + val payload = tokenPayload ?: return false + + // Check expiration + val expiration = payload.exp ?: return false + val currentTime = kotlin.time.Clock.System.now().epochSeconds + + return currentTime < expiration + } + + /** + * Clear token from memory (logout) + */ + fun clearToken() { + currentToken = null + tokenPayload = null + + _authState.value = AuthState() + } + + /** + * Get user ID from token + */ + fun getUserId(): String? = tokenPayload?.sub + + /** + * Get username from token + */ + fun getUsername(): String? = tokenPayload?.username + + /** + * Get current user permissions + */ + fun getPermissions(): List = _authState.value.permissions + + /** + * Check if user has a specific permission + */ + fun hasPermission(permission: Permission): Boolean { + return _authState.value.permissions.contains(permission) + } + + /** + * Check if user has any of the specified permissions + */ + fun hasAnyPermission(vararg permissions: Permission): Boolean { + return permissions.any { _authState.value.permissions.contains(it) } + } + + /** + * Check if user has all of the specified permissions + */ + fun hasAllPermissions(vararg permissions: Permission): Boolean { + return permissions.all { _authState.value.permissions.contains(it) } + } + + /** + * Check if user can perform read operations + */ + fun canRead(): Boolean { + return hasAnyPermission( + Permission.PERSON_READ, + Permission.VEREIN_READ, + Permission.VERANSTALTUNG_READ, + Permission.PFERD_READ + ) + } + + /** + * Check if user can perform create operations + */ + fun canCreate(): Boolean { + return hasAnyPermission( + Permission.PERSON_CREATE, + Permission.VEREIN_CREATE, + Permission.VERANSTALTUNG_CREATE, + Permission.PFERD_CREATE + ) + } + + /** + * Check if user can perform update operations + */ + fun canUpdate(): Boolean { + return hasAnyPermission( + Permission.PERSON_UPDATE, + Permission.VEREIN_UPDATE, + Permission.VERANSTALTUNG_UPDATE, + Permission.PFERD_UPDATE + ) + } + + /** + * Check if user can perform delete operations (admin-level) + */ + fun canDelete(): Boolean { + return hasAnyPermission( + Permission.PERSON_DELETE, + Permission.VEREIN_DELETE, + Permission.VERANSTALTUNG_DELETE, + Permission.PFERD_DELETE + ) + } + + /** + * Check if user is admin (has delete permissions) + */ + fun isAdmin(): Boolean = canDelete() + + /** + * Check if token expires within specified minutes + */ + @OptIn(ExperimentalTime::class) + fun isTokenExpiringSoon(minutesThreshold: Int = 5): Boolean { + val payload = tokenPayload ?: return false + val expiration = payload.exp ?: return false + val currentTime = kotlin.time.Clock.System.now().epochSeconds + val thresholdTime = currentTime + (minutesThreshold * 60) + + return expiration <= thresholdTime + } + + /** + * Parse JWT payload for basic validation and user info extraction + * Note: This is for client-side info extraction only, not security validation + */ + @OptIn(ExperimentalEncodingApi::class) + private fun parseJwtPayload(token: String): JwtPayload? { + return try { + val parts = token.split(".") + if (parts.size != 3) return null + + // Decode the payload (second part) + val payloadJson = Base64.decode(parts[1]).decodeToString() + + // First try to parse with standard approach + val basicPayload = try { + Json.decodeFromString(payloadJson) + } catch (e: Exception) { + // If that fails, extract manually + null + } + + // If basic parsing succeeded and has permissions, return it + if (basicPayload != null && basicPayload.permissions != null) { + return basicPayload + } + + // Otherwise, extract permissions manually from JSON string + val permissions = extractPermissionsFromJson(payloadJson) + + // Return payload with manually extracted permissions + JwtPayload( + sub = basicPayload?.sub, + username = basicPayload?.username, + exp = basicPayload?.exp, + iat = basicPayload?.iat, + iss = basicPayload?.iss, + permissions = permissions + ) + } catch (e: Exception) { + // Failed to parse - token might be invalid format + null + } + } + + /** + * Extract permissions array from JSON string using simple string parsing + */ + private fun extractPermissionsFromJson(jsonString: String): List? { + return try { + // Simple regex to find permissions array + val permissionsRegex = """"permissions":\s*\[(.*?)\]""".toRegex() + val match = permissionsRegex.find(jsonString) + + match?.let { + val permissionsContent = it.groupValues[1] + if (permissionsContent.isBlank()) return emptyList() + + // Extract individual permission strings + val permissions = permissionsContent + .split(",") + .mapNotNull { permission -> + permission.trim() + .removePrefix("\"") + .removeSuffix("\"") + .takeIf { it.isNotBlank() } + } + permissions + } + } catch (e: Exception) { + null + } + } + + /** + * Get token with Bearer prefix for HTTP headers + */ + fun getBearerToken(): String? { + val token = getToken() ?: return null + return "Bearer $token" + } + + /** + * Refresh token if needed based on expiry + */ + suspend fun refreshTokenIfNeeded(authApiClient: AuthApiClient): Boolean { + if (!isTokenExpiringSoon()) return true + + val currentToken = getToken() ?: return false + + val refreshResponse = authApiClient.refreshToken(currentToken) + if (refreshResponse.success && refreshResponse.token != null) { + setToken(refreshResponse.token) + return true } - /** - * Get current JWT token - */ - fun getToken(): String? = currentToken - - /** - * Check if we have a valid (non-expired) token - */ - @OptIn(ExperimentalTime::class) - fun hasValidToken(): Boolean { - val token = currentToken ?: return false - val payload = tokenPayload ?: return false - - // Check expiration - val expiration = payload.exp ?: return false - val currentTime = kotlin.time.Clock.System.now().epochSeconds - - return currentTime < expiration - } - - /** - * Clear token from memory (logout) - */ - fun clearToken() { - currentToken = null - tokenPayload = null - - _authState.value = AuthState() - } - - /** - * Get user ID from token - */ - fun getUserId(): String? = tokenPayload?.sub - - /** - * Get username from token - */ - fun getUsername(): String? = tokenPayload?.username - - /** - * Get current user permissions - */ - fun getPermissions(): List = _authState.value.permissions - - /** - * Check if user has a specific permission - */ - fun hasPermission(permission: Permission): Boolean { - return _authState.value.permissions.contains(permission) - } - - /** - * Check if user has any of the specified permissions - */ - fun hasAnyPermission(vararg permissions: Permission): Boolean { - return permissions.any { _authState.value.permissions.contains(it) } - } - - /** - * Check if user has all of the specified permissions - */ - fun hasAllPermissions(vararg permissions: Permission): Boolean { - return permissions.all { _authState.value.permissions.contains(it) } - } - - /** - * Check if user can perform read operations - */ - fun canRead(): Boolean { - return hasAnyPermission( - Permission.PERSON_READ, - Permission.VEREIN_READ, - Permission.VERANSTALTUNG_READ, - Permission.PFERD_READ - ) - } - - /** - * Check if user can perform create operations - */ - fun canCreate(): Boolean { - return hasAnyPermission( - Permission.PERSON_CREATE, - Permission.VEREIN_CREATE, - Permission.VERANSTALTUNG_CREATE, - Permission.PFERD_CREATE - ) - } - - /** - * Check if user can perform update operations - */ - fun canUpdate(): Boolean { - return hasAnyPermission( - Permission.PERSON_UPDATE, - Permission.VEREIN_UPDATE, - Permission.VERANSTALTUNG_UPDATE, - Permission.PFERD_UPDATE - ) - } - - /** - * Check if user can perform delete operations (admin-level) - */ - fun canDelete(): Boolean { - return hasAnyPermission( - Permission.PERSON_DELETE, - Permission.VEREIN_DELETE, - Permission.VERANSTALTUNG_DELETE, - Permission.PFERD_DELETE - ) - } - - /** - * Check if user is admin (has delete permissions) - */ - fun isAdmin(): Boolean = canDelete() - - /** - * Check if token expires within specified minutes - */ - @OptIn(ExperimentalTime::class) - fun isTokenExpiringSoon(minutesThreshold: Int = 5): Boolean { - val payload = tokenPayload ?: return false - val expiration = payload.exp ?: return false - val currentTime = kotlin.time.Clock.System.now().epochSeconds - val thresholdTime = currentTime + (minutesThreshold * 60) - - return expiration <= thresholdTime - } - - /** - * Parse JWT payload for basic validation and user info extraction - * Note: This is for client-side info extraction only, not security validation - */ - @OptIn(ExperimentalEncodingApi::class) - private fun parseJwtPayload(token: String): JwtPayload? { - return try { - val parts = token.split(".") - if (parts.size != 3) return null - - // Decode the payload (second part) - val payloadJson = Base64.decode(parts[1]).decodeToString() - - // First try to parse with standard approach - val basicPayload = try { - Json.decodeFromString(payloadJson) - } catch (e: Exception) { - // If that fails, extract manually - null - } - - // If basic parsing succeeded and has permissions, return it - if (basicPayload != null && basicPayload.permissions != null) { - return basicPayload - } - - // Otherwise, extract permissions manually from JSON string - val permissions = extractPermissionsFromJson(payloadJson) - - // Return payload with manually extracted permissions - JwtPayload( - sub = basicPayload?.sub, - username = basicPayload?.username, - exp = basicPayload?.exp, - iat = basicPayload?.iat, - iss = basicPayload?.iss, - permissions = permissions - ) - } catch (e: Exception) { - // Failed to parse - token might be invalid format - null - } - } - - /** - * Extract permissions array from JSON string using simple string parsing - */ - private fun extractPermissionsFromJson(jsonString: String): List? { - return try { - // Simple regex to find permissions array - val permissionsRegex = """"permissions":\s*\[(.*?)\]""".toRegex() - val match = permissionsRegex.find(jsonString) - - match?.let { - val permissionsContent = it.groupValues[1] - if (permissionsContent.isBlank()) return emptyList() - - // Extract individual permission strings - val permissions = permissionsContent - .split(",") - .mapNotNull { permission -> - permission.trim() - .removePrefix("\"") - .removeSuffix("\"") - .takeIf { it.isNotBlank() } - } - permissions - } - } catch (e: Exception) { - null - } - } - - /** - * Get token with Bearer prefix for HTTP headers - */ - fun getBearerToken(): String? { - val token = getToken() ?: return null - return "Bearer $token" - } - - /** - * Refresh token if needed based on expiry - */ - suspend fun refreshTokenIfNeeded(authApiClient: AuthApiClient): Boolean { - if (!isTokenExpiringSoon()) return true - - val currentToken = getToken() ?: return false - - val refreshResponse = authApiClient.refreshToken(currentToken) - if (refreshResponse.success && refreshResponse.token != null) { - setToken(refreshResponse.token) - return true - } - - // Refresh failed, clear token - clearToken() - return false - } + // Refresh failed, clear token + clearToken() + return false + } } diff --git a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthenticatedHttpClient.kt b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthenticatedHttpClient.kt index b3e341fa..75088212 100644 --- a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthenticatedHttpClient.kt +++ b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthenticatedHttpClient.kt @@ -1,6 +1,6 @@ package at.mocode.clients.authfeature -import at.mocode.clients.shared.AppConfig +import at.mocode.clients.shared.core.AppConstants import io.ktor.client.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* @@ -14,49 +14,49 @@ import kotlinx.serialization.json.Json */ object AuthenticatedHttpClient { - private val authTokenManager = AuthTokenManager() + private val authTokenManager = AuthTokenManager() - /** - * Create a basic HTTP client with JSON support - */ - fun create(baseUrl: String = AppConfig.GATEWAY_URL): HttpClient { - return HttpClient { - install(ContentNegotiation) { - json(Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }) - } - } + /** + * Create a basic HTTP client with JSON support + */ + fun create(baseUrl: String = AppConstants.GATEWAY_URL): HttpClient { + return HttpClient { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }) + } } + } - /** - * Add an authentication header to an HTTP request builder if a token is available - */ - fun HttpRequestBuilder.addAuthHeader() { - authTokenManager.getBearerToken()?.let { bearerToken -> - header(HttpHeaders.Authorization, bearerToken) - } + /** + * Add an authentication header to an HTTP request builder if a token is available + */ + fun HttpRequestBuilder.addAuthHeader() { + authTokenManager.getBearerToken()?.let { bearerToken -> + header(HttpHeaders.Authorization, bearerToken) } + } - /** - * Get the shared AuthTokenManager instance - */ - fun getAuthTokenManager(): AuthTokenManager = authTokenManager + /** + * Get the shared AuthTokenManager instance + */ + fun getAuthTokenManager(): AuthTokenManager = authTokenManager - /** - * Create an HTTP client without authentication (for login/public endpoints) - */ - fun createUnauthenticated(): HttpClient { - return HttpClient { - install(ContentNegotiation) { - json(Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }) - } - } + /** + * Create an HTTP client without authentication (for login/public endpoints) + */ + fun createUnauthenticated(): HttpClient { + return HttpClient { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }) + } } + } } diff --git a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginScreen.kt b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginScreen.kt index 14f91ee0..4a4f9cb2 100644 --- a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginScreen.kt +++ b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginScreen.kt @@ -19,118 +19,118 @@ import androidx.lifecycle.viewmodel.compose.viewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginScreen( - authTokenManager: AuthTokenManager, - viewModel: LoginViewModel = viewModel { LoginViewModel(authTokenManager) }, - onLoginSuccess: () -> Unit = {} + authTokenManager: AuthTokenManager, + viewModel: LoginViewModel = viewModel { LoginViewModel(authTokenManager) }, + onLoginSuccess: () -> Unit = {} ) { - val uiState by viewModel.uiState.collectAsState() - val passwordFocusRequester = remember { FocusRequester() } + val uiState by viewModel.uiState.collectAsState() + val passwordFocusRequester = remember { FocusRequester() } - Column( + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Title + Text( + text = "Anmelden", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 32.dp) + ) + + // Username field + OutlinedTextField( + value = uiState.username, + onValueChange = viewModel::updateUsername, + label = { Text("Benutzername") }, + enabled = !uiState.isLoading, + isError = uiState.usernameError != null, + supportingText = uiState.usernameError?.let { { Text(it) } }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onNext = { passwordFocusRequester.requestFocus() } + ), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) + + // Password field + OutlinedTextField( + value = uiState.password, + onValueChange = viewModel::updatePassword, + label = { Text("Passwort") }, + enabled = !uiState.isLoading, + isError = uiState.passwordError != null, + supportingText = uiState.passwordError?.let { { Text(it) } }, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + if (uiState.canLogin) { + viewModel.login() + } + } + ), + modifier = Modifier + .fillMaxWidth() + .focusRequester(passwordFocusRequester) + .padding(bottom = 24.dp) + ) + + // Error message + if (uiState.errorMessage != null) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ), modifier = Modifier - .fillMaxSize() - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - // Title + .fillMaxWidth() + .padding(bottom = 16.dp) + ) { Text( - text = "Anmelden", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 32.dp) + text = uiState.errorMessage!!, + color = MaterialTheme.colorScheme.onErrorContainer, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = Modifier.padding(16.dp) ) - - // Username field - OutlinedTextField( - value = uiState.username, - onValueChange = viewModel::updateUsername, - label = { Text("Benutzername") }, - enabled = !uiState.isLoading, - isError = uiState.usernameError != null, - supportingText = uiState.usernameError?.let { { Text(it) } }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next - ), - keyboardActions = KeyboardActions( - onNext = { passwordFocusRequester.requestFocus() } - ), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp) - ) - - // Password field - OutlinedTextField( - value = uiState.password, - onValueChange = viewModel::updatePassword, - label = { Text("Passwort") }, - enabled = !uiState.isLoading, - isError = uiState.passwordError != null, - supportingText = uiState.passwordError?.let { { Text(it) } }, - visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { - if (uiState.canLogin) { - viewModel.login() - } - } - ), - modifier = Modifier - .fillMaxWidth() - .focusRequester(passwordFocusRequester) - .padding(bottom = 24.dp) - ) - - // Error message - if (uiState.errorMessage != null) { - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp) - ) { - Text( - text = uiState.errorMessage!!, - color = MaterialTheme.colorScheme.onErrorContainer, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - modifier = Modifier.padding(16.dp) - ) - } - } - - // Login button - Button( - onClick = { viewModel.login() }, - enabled = uiState.canLogin && !uiState.isLoading, - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - ) { - if (uiState.isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text("Anmelden") - } - } + } } - // Handle login success - LaunchedEffect(uiState.isAuthenticated) { - if (uiState.isAuthenticated) { - onLoginSuccess() - } + // Login button + Button( + onClick = { viewModel.login() }, + enabled = uiState.canLogin && !uiState.isLoading, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + ) { + if (uiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Anmelden") + } } + } + + // Handle login success + LaunchedEffect(uiState.isAuthenticated) { + if (uiState.isAuthenticated) { + onLoginSuccess() + } + } } diff --git a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginViewModel.kt b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginViewModel.kt index b6686039..8a603c72 100644 --- a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginViewModel.kt +++ b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginViewModel.kt @@ -2,131 +2,130 @@ package at.mocode.clients.authfeature import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import at.mocode.clients.shared.AppConfig -import io.ktor.client.call.* +import at.mocode.clients.authfeature.AuthenticatedHttpClient.addAuthHeader +import at.mocode.clients.shared.core.AppConstants import io.ktor.client.request.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import at.mocode.clients.authfeature.AuthenticatedHttpClient.addAuthHeader /** * UI state for the login screen */ data class LoginUiState( - val username: String = "", - val password: String = "", - val isLoading: Boolean = false, - val isAuthenticated: Boolean = false, - val errorMessage: String? = null, - val usernameError: String? = null, - val passwordError: String? = null + val username: String = "", + val password: String = "", + val isLoading: Boolean = false, + val isAuthenticated: Boolean = false, + val errorMessage: String? = null, + val usernameError: String? = null, + val passwordError: String? = null ) { - val canLogin: Boolean - get() = username.isNotBlank() && password.isNotBlank() && !isLoading + val canLogin: Boolean + get() = username.isNotBlank() && password.isNotBlank() && !isLoading } /** * ViewModel for handling login authentication logic */ class LoginViewModel( - private val authTokenManager: AuthTokenManager + private val authTokenManager: AuthTokenManager ) : ViewModel() { - private val _uiState = MutableStateFlow(LoginUiState()) - val uiState: StateFlow = _uiState.asStateFlow() + private val _uiState = MutableStateFlow(LoginUiState()) + val uiState: StateFlow = _uiState.asStateFlow() - private val authApiClient = AuthApiClient() + private val authApiClient = AuthApiClient() - fun updateUsername(username: String) { - _uiState.value = _uiState.value.copy( - username = username, - usernameError = null, - errorMessage = null - ) + fun updateUsername(username: String) { + _uiState.value = _uiState.value.copy( + username = username, + usernameError = null, + errorMessage = null + ) + } + + fun updatePassword(password: String) { + _uiState.value = _uiState.value.copy( + password = password, + passwordError = null, + errorMessage = null + ) + } + + fun login() { + val currentState = _uiState.value + + // Validate input + if (currentState.username.isBlank()) { + _uiState.value = currentState.copy(usernameError = "Benutzername ist erforderlich") + return } - fun updatePassword(password: String) { - _uiState.value = _uiState.value.copy( - password = password, - passwordError = null, - errorMessage = null - ) + if (currentState.password.isBlank()) { + _uiState.value = currentState.copy(passwordError = "Passwort ist erforderlich") + return } - fun login() { - val currentState = _uiState.value + // Start the login process + _uiState.value = currentState.copy( + isLoading = true, + errorMessage = null, + usernameError = null, + passwordError = null + ) - // Validate input - if (currentState.username.isBlank()) { - _uiState.value = currentState.copy(usernameError = "Benutzername ist erforderlich") - return - } - - if (currentState.password.isBlank()) { - _uiState.value = currentState.copy(passwordError = "Passwort ist erforderlich") - return - } - - // Start the login process - _uiState.value = currentState.copy( - isLoading = true, - errorMessage = null, - usernameError = null, - passwordError = null + viewModelScope.launch { + try { + val loginResponse = authApiClient.login( + username = currentState.username, + password = currentState.password ) - viewModelScope.launch { + if (loginResponse.success && loginResponse.token != null) { + // Store the JWT token + authTokenManager.setToken(loginResponse.token) + + _uiState.value = _uiState.value.copy( + isLoading = false, + isAuthenticated = true, + errorMessage = null + ) + + // Fire-and-forget: Trigger Backend Sync so the user exists in Members + viewModelScope.launch { try { - val loginResponse = authApiClient.login( - username = currentState.username, - password = currentState.password - ) - - if (loginResponse.success && loginResponse.token != null) { - // Store the JWT token - authTokenManager.setToken(loginResponse.token) - - _uiState.value = _uiState.value.copy( - isLoading = false, - isAuthenticated = true, - errorMessage = null - ) - - // Fire-and-forget: Trigger Backend Sync so the user exists in Members - viewModelScope.launch { - try { - val client = AuthenticatedHttpClient.create() - client.post("${AppConfig.GATEWAY_URL}/api/members/sync") { - addAuthHeader() - } - } catch (_: Exception) { - // Non-fatal: Wir zeigen Sync-Fehler im Login nicht an - } - } - } else { - _uiState.value = _uiState.value.copy( - isLoading = false, - errorMessage = loginResponse.message ?: "Anmeldung fehlgeschlagen" - ) - } - } catch (e: Exception) { - _uiState.value = _uiState.value.copy( - isLoading = false, - errorMessage = "Verbindungsfehler: ${e.message}" - ) + val client = AuthenticatedHttpClient.create() + client.post("${AppConstants.GATEWAY_URL}/api/members/sync") { + addAuthHeader() + } + } catch (_: Exception) { + // Non-fatal: Wir zeigen Sync-Fehler im Login nicht an } + } + } else { + _uiState.value = _uiState.value.copy( + isLoading = false, + errorMessage = loginResponse.message ?: "Anmeldung fehlgeschlagen" + ) } + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + isLoading = false, + errorMessage = "Verbindungsfehler: ${e.message}" + ) + } } + } - fun logout() { - authTokenManager.clearToken() - _uiState.value = LoginUiState() - } + fun logout() { + authTokenManager.clearToken() + _uiState.value = LoginUiState() + } - fun checkAuthenticationStatus() { - val isAuthenticated = authTokenManager.hasValidToken() - _uiState.value = _uiState.value.copy(isAuthenticated = isAuthenticated) - } + fun checkAuthenticationStatus() { + val isAuthenticated = authTokenManager.hasValidToken() + _uiState.value = _uiState.value.copy(isAuthenticated = isAuthenticated) + } } diff --git a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/oauth/AuthCallbackParams.kt b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/oauth/AuthCallbackParams.kt new file mode 100644 index 00000000..ffe6ddfe --- /dev/null +++ b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/oauth/AuthCallbackParams.kt @@ -0,0 +1,12 @@ +package at.mocode.clients.authfeature.oauth + +data class CallbackParams(val code: String, val state: String?) + +expect object AuthCallbackParams { + /** + * Parse OAuth callback parameters from the current environment. + * - JS (web): reads window.location.search for `code` and `state` and removes them from the URL. + * - JVM (desktop): returns null. + */ + fun parse(): CallbackParams? +} diff --git a/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/oauth/OAuthPkce.kt b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/oauth/OAuthPkce.kt new file mode 100644 index 00000000..b5bbad0b --- /dev/null +++ b/clients/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/oauth/OAuthPkce.kt @@ -0,0 +1,34 @@ +package at.mocode.clients.authfeature.oauth + +import at.mocode.clients.shared.core.AppConstants + +data class PkceState( + val state: String, + val codeVerifier: String, + val codeChallenge: String, + val method: String = "S256" +) + +object OAuthParams { + const val RESPONSE_TYPE = "code" + const val SCOPE = "openid" +} + +/** + * expect/actual service to support PKCE across JS and JVM. + * For the desktop (JVM) target we currently do not start a browser flow, + * but we provide hashing to keep API parity. + */ +expect object OAuthPkceService { + /** Starts a PKCE auth attempt and stores transient state in memory. */ + suspend fun startAuth(): PkceState + + /** Returns currently active state if any (not persisted). */ + fun current(): PkceState? + + /** Clears transient state (after success/failure). */ + fun clear() + + /** Builds the authorize URL for the current state. */ + fun buildAuthorizeUrl(state: PkceState, redirectUri: String = AppConstants.webRedirectUri()): String +} diff --git a/clients/auth-feature/src/jsMain/kotlin/at/mocode/clients/authfeature/oauth/AuthCallbackParamsJs.kt b/clients/auth-feature/src/jsMain/kotlin/at/mocode/clients/authfeature/oauth/AuthCallbackParamsJs.kt new file mode 100644 index 00000000..265319c1 --- /dev/null +++ b/clients/auth-feature/src/jsMain/kotlin/at/mocode/clients/authfeature/oauth/AuthCallbackParamsJs.kt @@ -0,0 +1,19 @@ +package at.mocode.clients.authfeature.oauth + +import kotlinx.browser.window + +actual object AuthCallbackParams { + actual fun parse(): CallbackParams? { + val search = window.location.search + if (search.isBlank()) return null + val params = js("new URLSearchParams(arguments[0])").unsafeCast<(String) -> dynamic>()(search) + val code = params.get("code") as String? + val state = params.get("state") as String? + return if (!code.isNullOrBlank()) { + // Clean up query params to avoid re-processing on recomposition + val url = window.location.origin + window.location.pathname + window.history.replaceState(null, "", url) + CallbackParams(code, state) + } else null + } +} diff --git a/clients/auth-feature/src/jsMain/kotlin/at/mocode/clients/authfeature/oauth/OAuthPkceJs.kt b/clients/auth-feature/src/jsMain/kotlin/at/mocode/clients/authfeature/oauth/OAuthPkceJs.kt new file mode 100644 index 00000000..c741a1b1 --- /dev/null +++ b/clients/auth-feature/src/jsMain/kotlin/at/mocode/clients/authfeature/oauth/OAuthPkceJs.kt @@ -0,0 +1,81 @@ +package at.mocode.clients.authfeature.oauth + +import at.mocode.clients.shared.core.AppConstants +import kotlinx.browser.window +import kotlinx.coroutines.await +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Uint8Array +import kotlin.js.Promise +import kotlin.random.Random + +private var currentPkce: PkceState? = null + +private fun base64UrlFromBytes(bytes: ByteArray): String { + // Build binary string from bytes + val sb = StringBuilder(bytes.size) + for (b in bytes) sb.append(b.toInt().toChar()) + val b64 = window.btoa(sb.toString()) + return b64.replace("+", "-").replace("/", "_").trimEnd('=') +} + +private fun base64UrlFromArrayBuffer(buf: ArrayBuffer): String { + val arr = Uint8Array(buf) + var binary = "" + val len = arr.length + for (i in 0 until len) { + val v = (arr.asDynamic()[i] as Number).toInt() + binary += fromCharCode(v) + } + val b64 = window.btoa(binary) + return b64.replace("+", "-").replace("/", "_").trimEnd('=') +} + +private fun randomUrlSafe(length: Int): String { + val bytes = Random.Default.nextBytes(length) + // Use base64url for entropy; ensure URL-safe by replacing padding removed already + return base64UrlFromBytes(bytes) +} + +private fun sha256(input: String): Promise { + val enc: dynamic = js("new TextEncoder()") + val data = enc.encode(input) + val subtle: dynamic = window.asDynamic().crypto.subtle + return subtle.digest("SHA-256", data) as Promise +} + +actual object OAuthPkceService { + actual suspend fun startAuth(): PkceState { + val codeVerifier = randomUrlSafe(64) + val challengeBuf = sha256(codeVerifier).await() + val codeChallenge = base64UrlFromArrayBuffer(challengeBuf) + val state = randomUrlSafe(16) + val pkce = PkceState(state, codeVerifier, codeChallenge) + currentPkce = pkce + return pkce + } + + actual fun current(): PkceState? = currentPkce + + actual fun clear() { + currentPkce = null + } + + actual fun buildAuthorizeUrl(state: PkceState, redirectUri: String): String { + val params = listOf( + "response_type" to OAuthParams.RESPONSE_TYPE, + "client_id" to AppConstants.KEYCLOAK_CLIENT_ID, + "redirect_uri" to redirectUri, + "scope" to OAuthParams.SCOPE, + "state" to state.state, + "code_challenge" to state.codeChallenge, + "code_challenge_method" to state.method + ).joinToString("&") { (k, v) -> "$k=" + encodeURIComponent(v) } + return AppConstants.authorizeEndpoint() + "?" + params + } +} + +@Suppress("UnsafeCastFromDynamic") +private fun encodeURIComponent(value: String): String = js("encodeURIComponent")(value) + +@Suppress("UnsafeCastFromDynamic") +private fun fromCharCode(code: Int): String = js("String.fromCharCode")(code) diff --git a/clients/auth-feature/src/jvmMain/kotlin/at/mocode/clients/authfeature/oauth/AuthCallbackParamsJvm.kt b/clients/auth-feature/src/jvmMain/kotlin/at/mocode/clients/authfeature/oauth/AuthCallbackParamsJvm.kt new file mode 100644 index 00000000..1b6552de --- /dev/null +++ b/clients/auth-feature/src/jvmMain/kotlin/at/mocode/clients/authfeature/oauth/AuthCallbackParamsJvm.kt @@ -0,0 +1,5 @@ +package at.mocode.clients.authfeature.oauth + +actual object AuthCallbackParams { + actual fun parse(): CallbackParams? = null +} diff --git a/clients/auth-feature/src/jvmMain/kotlin/at/mocode/clients/authfeature/oauth/OAuthPkceJvm.kt b/clients/auth-feature/src/jvmMain/kotlin/at/mocode/clients/authfeature/oauth/OAuthPkceJvm.kt new file mode 100644 index 00000000..d8b7dba2 --- /dev/null +++ b/clients/auth-feature/src/jvmMain/kotlin/at/mocode/clients/authfeature/oauth/OAuthPkceJvm.kt @@ -0,0 +1,55 @@ +package at.mocode.clients.authfeature.oauth + +import at.mocode.clients.shared.core.AppConstants +import java.security.MessageDigest +import java.security.SecureRandom +import java.util.Base64 + +private var currentPkceJvm: PkceState? = null + +private fun base64UrlNoPad(bytes: ByteArray): String = + Base64.getUrlEncoder().withoutPadding().encodeToString(bytes) + +private fun randomUrlSafe(length: Int): String { + // Generate bytes and Base64 URL encode (will be > length due to encoding) + val rnd = SecureRandom() + val bytes = ByteArray(length) + rnd.nextBytes(bytes) + return base64UrlNoPad(bytes) +} + +private fun sha256Base64Url(input: String): String { + val md = MessageDigest.getInstance("SHA-256") + val digest = md.digest(input.toByteArray(Charsets.UTF_8)) + return base64UrlNoPad(digest) +} + +actual object OAuthPkceService { + actual suspend fun startAuth(): PkceState { + val codeVerifier = randomUrlSafe(64) + val codeChallenge = sha256Base64Url(codeVerifier) + val state = randomUrlSafe(16) + val pkce = PkceState(state, codeVerifier, codeChallenge) + currentPkceJvm = pkce + return pkce + } + + actual fun current(): PkceState? = currentPkceJvm + + actual fun clear() { + currentPkceJvm = null + } + + actual fun buildAuthorizeUrl(state: PkceState, redirectUri: String): String { + val params = listOf( + "response_type" to OAuthParams.RESPONSE_TYPE, + "client_id" to AppConstants.KEYCLOAK_CLIENT_ID, + "redirect_uri" to redirectUri, + "scope" to OAuthParams.SCOPE, + "state" to state.state, + "code_challenge" to state.codeChallenge, + "code_challenge_method" to state.method + ).joinToString("&") { (k, v) -> "$k=" + java.net.URLEncoder.encode(v, Charsets.UTF_8) } + return AppConstants.authorizeEndpoint() + "?" + params + } +} diff --git a/clients/members-feature/build.gradle.kts b/clients/members-feature/build.gradle.kts deleted file mode 100644 index a712a6ca..00000000 --- a/clients/members-feature/build.gradle.kts +++ /dev/null @@ -1,87 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - alias(libs.plugins.kotlinSerialization) -} - -group = "at.mocode.clients" -version = "1.0.0" - -kotlin { - val enableWasm = providers.gradleProperty("enableWasm").orNull == "true" - - jvmToolchain(21) - - jvm() - - js { - browser { - testTask { enabled = false } - } - } - - if (enableWasm) { - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { browser() } - } - - sourceSets { - commonMain.dependencies { - // UI Kit - implementation(project(":clients:shared:common-ui")) - // Shared config/utilities - implementation(project(":clients:shared")) - // Authentication helpers (token + authenticated client) - implementation(project(":clients:auth-feature")) - - // Compose - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.ui) - implementation(compose.components.resources) - implementation(compose.materialIconsExtended) - - // ViewModel lifecycle + compose helpers - implementation(libs.bundles.compose.common) - - // HTTP + Kotlinx - implementation(libs.bundles.ktor.client.common) - implementation(libs.bundles.kotlinx.core) - } - - commonTest.dependencies { - implementation(libs.kotlin.test) - implementation(libs.kotlinx.coroutines.test) - implementation(libs.ktor.client.mock) - } - - jvmMain.dependencies { - implementation(libs.ktor.client.cio) - } - - jsMain.dependencies { - implementation(libs.ktor.client.js) - } - - if (enableWasm) { - val wasmJsMain = getByName("wasmJsMain") - wasmJsMain.dependencies { - implementation(libs.ktor.client.js) - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material3) - } - } - } -} - -tasks.withType { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - freeCompilerArgs.addAll("-opt-in=kotlin.RequiresOptIn") - } -} diff --git a/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/MembersApiClient.kt b/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/MembersApiClient.kt deleted file mode 100644 index ebc2d19f..00000000 --- a/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/MembersApiClient.kt +++ /dev/null @@ -1,37 +0,0 @@ -package at.mocode.clients.membersfeature - -import at.mocode.clients.authfeature.AuthenticatedHttpClient -import at.mocode.clients.shared.AppConfig -import at.mocode.clients.membersfeature.model.MemberProfile -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.http.* -import at.mocode.clients.authfeature.AuthenticatedHttpClient.addAuthHeader - -class MembersApiClient( - private val baseUrl: String = AppConfig.GATEWAY_URL -) { - private val client = AuthenticatedHttpClient.create() - - suspend fun getMyProfile(): MemberProfile { - // Erwarteter Endpoint: GET /api/members/me - return client.get("$baseUrl/api/members/me") { - addAuthHeader() - }.body() - } - - /** - * Optionaler Convenience-Call: Löst den Backend-Sync einmalig aus. - * Gibt true zurück, wenn der Call erfolgreich war (HTTP 2xx), sonst false. - */ - suspend fun syncProfile(): Boolean { - return try { - val response = client.post("$baseUrl/api/members/sync") { - addAuthHeader() - } - response.status.isSuccess() - } catch (_: Exception) { - false - } - } -} diff --git a/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/ProfileScreen.kt b/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/ProfileScreen.kt deleted file mode 100644 index 5836fd50..00000000 --- a/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/ProfileScreen.kt +++ /dev/null @@ -1,63 +0,0 @@ -package at.mocode.clients.membersfeature - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun ProfileScreen(viewModel: ProfileViewModel) { - val state by viewModel.uiState.collectAsState() - - LaunchedEffect(Unit) { - viewModel.loadProfile() - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text(text = "👤 Mein Profil", style = MaterialTheme.typography.headlineSmall) - - if (state.isLoading) { - Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } - - state.errorMessage?.let { error -> - Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Fehler", style = MaterialTheme.typography.titleMedium) - Spacer(Modifier.height(8.dp)) - Text(error) - Spacer(Modifier.height(8.dp)) - Button(onClick = { viewModel.clearError() }) { Text("Schließen") } - } - } - } - - state.profile?.let { profile -> - Card { - Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text(text = profile.fullName.ifBlank { "Unbekannt" }, style = MaterialTheme.typography.titleLarge) - profile.username?.let { Text("Benutzername: $it") } - profile.email?.let { Text("E-Mail: $it") } - if (profile.roles.isNotEmpty()) { - Text("Rollen: ${profile.roles.joinToString(", ")}") - } - } - } - } - - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button(onClick = { viewModel.loadProfile() }, enabled = !state.isLoading) { - Text("Neu laden") - } - } - } -} diff --git a/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/ProfileViewModel.kt b/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/ProfileViewModel.kt deleted file mode 100644 index 24f03809..00000000 --- a/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/ProfileViewModel.kt +++ /dev/null @@ -1,42 +0,0 @@ -package at.mocode.clients.membersfeature - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import at.mocode.clients.membersfeature.model.MemberProfile -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch - -data class ProfileUiState( - val isLoading: Boolean = false, - val profile: MemberProfile? = null, - val errorMessage: String? = null -) - -class ProfileViewModel( - private val api: MembersApiClient = MembersApiClient() -) : ViewModel() { - - private val _uiState = MutableStateFlow(ProfileUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - fun loadProfile() { - _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null) - viewModelScope.launch { - try { - val profile = api.getMyProfile() - _uiState.value = ProfileUiState(isLoading = false, profile = profile) - } catch (e: Exception) { - _uiState.value = ProfileUiState( - isLoading = false, - errorMessage = e.message ?: "Profil konnte nicht geladen werden" - ) - } - } - } - - fun clearError() { - _uiState.value = _uiState.value.copy(errorMessage = null) - } -} diff --git a/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/model/MemberProfile.kt b/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/model/MemberProfile.kt deleted file mode 100644 index 9bd2726c..00000000 --- a/clients/members-feature/src/commonMain/kotlin/at/mocode/clients/membersfeature/model/MemberProfile.kt +++ /dev/null @@ -1,16 +0,0 @@ -package at.mocode.clients.membersfeature.model - -import kotlinx.serialization.Serializable - -@Serializable -data class MemberProfile( - val id: String? = null, - val username: String? = null, - val email: String? = null, - val firstName: String? = null, - val lastName: String? = null, - val roles: List = emptyList() -) { - val fullName: String - get() = listOfNotNull(firstName, lastName).joinToString(" ").ifBlank { username ?: "" } -} diff --git a/clients/ping-feature/build.gradle.kts b/clients/ping-feature/build.gradle.kts index 55021653..25beb5c3 100644 --- a/clients/ping-feature/build.gradle.kts +++ b/clients/ping-feature/build.gradle.kts @@ -6,111 +6,115 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget * und den UI-Baukasten (common-ui), aber es kennt keine anderen Features. */ plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.kotlinSerialization) } group = "at.mocode.clients" version = "1.0.0" kotlin { - val enableWasm = providers.gradleProperty("enableWasm").orNull == "true" + val enableWasm = providers.gradleProperty("enableWasm").orNull == "true" - jvmToolchain(21) + jvmToolchain(21) - jvm() + jvm() + + js { + browser { + testTask { + enabled = false + } + } + binaries.executable() + } + + // WASM, nur wenn explizit aktiviert + if (enableWasm) { + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + } + + sourceSets { + commonMain.dependencies { + // Contract from backend + implementation(projects.services.ping.pingApi) + + // UI Kit + implementation(project(":clients:shared:common-ui")) + + // Shared Konfig & Utilities + implementation(project(":clients:shared")) + + // Compose dependencies + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + implementation(compose.components.resources) + implementation(compose.materialIconsExtended) + + // Ktor client for HTTP calls + implementation(libs.bundles.ktor.client.common) + + // Coroutines and serialization + implementation(libs.bundles.kotlinx.core) + + // ViewModel lifecycle + implementation(libs.bundles.compose.common) - js { - browser { - testTask { - enabled = false - } - } } - // WASM, nur wenn explizit aktiviert + commonTest.dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.ktor.client.mock) + + } + + jvmTest.dependencies { + implementation(libs.mockk) + implementation(projects.platform.platformTesting) + implementation(libs.bundles.testing.jvm) + } + + jvmMain.dependencies { + implementation(libs.ktor.client.cio) + // Auth-Models Zugriff (nur für JVM) + //implementation(project(":infrastructure:auth:auth-client")) + } + + jsMain.dependencies { + implementation(libs.ktor.client.js) + + } + + // WASM SourceSet, nur wenn aktiviert if (enableWasm) { - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { browser() } - } - - sourceSets { - commonMain.dependencies { - // Contract from backend - implementation(projects.services.ping.pingApi) - - // UI Kit - implementation(project(":clients:shared:common-ui")) - - // Shared Konfig & Utilities - implementation(project(":clients:shared")) - - // Compose dependencies - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.ui) - implementation(compose.components.resources) - implementation(compose.materialIconsExtended) - - // Ktor client for HTTP calls - implementation(libs.bundles.ktor.client.common) - - // Coroutines and serialization - implementation(libs.bundles.kotlinx.core) - - // ViewModel lifecycle - implementation(libs.bundles.compose.common) - - } - - commonTest.dependencies { - implementation(libs.kotlin.test) - implementation(libs.kotlinx.coroutines.test) - implementation(libs.ktor.client.mock) - - } - - jvmTest.dependencies { - implementation(libs.mockk) - implementation(projects.platform.platformTesting) - implementation(libs.bundles.testing.jvm) - } - - jvmMain.dependencies { - implementation(libs.ktor.client.cio) - // Auth-Models Zugriff (nur für JVM) - //implementation(project(":infrastructure:auth:auth-client")) - } - - jsMain.dependencies { - implementation(libs.ktor.client.js) - - } - - // WASM SourceSet, nur wenn aktiviert - if (enableWasm) { - val wasmJsMain = getByName("wasmJsMain") - wasmJsMain.dependencies { - implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7] - - // ✅ HINZUFÜGEN: Compose für shared UI components für WASM - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material3) - } - } + val wasmJsMain = getByName("wasmJsMain") + wasmJsMain.dependencies { + implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7] + + // ✅ HINZUFÜGEN: Compose für shared UI components für WASM + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + } } + } } // KMP Compile-Optionen tasks.withType { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - freeCompilerArgs.addAll( - "-opt-in=kotlin.RequiresOptIn" - ) - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + freeCompilerArgs.addAll( + "-opt-in=kotlin.RequiresOptIn" + ) + } } diff --git a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiClient.kt b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiClient.kt index 2f2948a9..142bada1 100644 --- a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiClient.kt +++ b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiClient.kt @@ -4,7 +4,7 @@ import at.mocode.ping.api.PingApi import at.mocode.ping.api.PingResponse import at.mocode.ping.api.EnhancedPingResponse import at.mocode.ping.api.HealthResponse -import at.mocode.clients.shared.AppConfig +import at.mocode.clients.shared.core.AppConstants import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.plugins.contentnegotiation.* @@ -13,30 +13,30 @@ import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json class PingApiClient( - private val baseUrl: String = AppConfig.GATEWAY_URL + private val baseUrl: String = AppConstants.GATEWAY_URL ) : PingApi { - private val client = HttpClient { - install(ContentNegotiation) { - json(Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }) - } + private val client = HttpClient { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }) } + } - override suspend fun simplePing(): PingResponse { - return client.get("$baseUrl/api/ping/simple").body() - } + override suspend fun simplePing(): PingResponse { + return client.get("$baseUrl/api/ping/simple").body() + } - override suspend fun enhancedPing(simulate: Boolean): EnhancedPingResponse { - return client.get("$baseUrl/api/ping/enhanced") { - parameter("simulate", simulate) - }.body() - } + override suspend fun enhancedPing(simulate: Boolean): EnhancedPingResponse { + return client.get("$baseUrl/api/ping/enhanced") { + parameter("simulate", simulate) + }.body() + } - override suspend fun healthCheck(): HealthResponse { - return client.get("$baseUrl/api/ping/health").body() - } + override suspend fun healthCheck(): HealthResponse { + return client.get("$baseUrl/api/ping/health").body() + } } diff --git a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingScreen.kt b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingScreen.kt index ab47d6ef..61d6fb35 100644 --- a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingScreen.kt +++ b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingScreen.kt @@ -21,288 +21,288 @@ import at.mocode.clients.pingfeature.model.RoleCategory @Composable fun PingScreen(viewModel: PingViewModel) { - val uiState = viewModel.uiState + val uiState = viewModel.uiState - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "Ping Service", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + + // Action Buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Text( - text = "Ping Service", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold - ) + Button( + onClick = { viewModel.performSimplePing() }, + enabled = !uiState.isLoading, + modifier = Modifier.weight(1f) + ) { + Text("Simple Ping") + } - // Action Buttons - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = { viewModel.performSimplePing() }, - enabled = !uiState.isLoading, - modifier = Modifier.weight(1f) - ) { - Text("Simple Ping") - } + Button( + onClick = { viewModel.performEnhancedPing() }, + enabled = !uiState.isLoading, + modifier = Modifier.weight(1f) + ) { + Text("Enhanced Ping") + } - Button( - onClick = { viewModel.performEnhancedPing() }, - enabled = !uiState.isLoading, - modifier = Modifier.weight(1f) - ) { - Text("Enhanced Ping") - } - - Button( - onClick = { viewModel.performHealthCheck() }, - enabled = !uiState.isLoading, - modifier = Modifier.weight(1f) - ) { - Text("Health Check") - } - } - - // Loading indicator - if (uiState.isLoading) { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } - - // Error message - uiState.errorMessage?.let { error -> - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Text( - text = "Error", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onErrorContainer, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = error, - color = MaterialTheme.colorScheme.onErrorContainer - ) - Spacer(modifier = Modifier.height(8.dp)) - Button( - onClick = { viewModel.clearError() } - ) { - Text("Dismiss") - } - } - } - } - - // Simple Ping Response - uiState.simplePingResponse?.let { response -> - ResponseCard( - title = "Simple Ping Response", - status = response.status, - timestamp = response.timestamp, - service = response.service - ) - } - - // Enhanced Ping Response - uiState.enhancedPingResponse?.let { response -> - ResponseCard( - title = "Enhanced Ping Response", - status = response.status, - timestamp = response.timestamp, - service = response.service, - additionalInfo = mapOf( - "Circuit Breaker State" to response.circuitBreakerState, - "Response Time" to "${response.responseTime}ms" - ) - ) - } - - // Health Response - uiState.healthResponse?.let { response -> - ResponseCard( - title = "Health Check Response", - status = response.status, - timestamp = response.timestamp, - service = response.service, - additionalInfo = mapOf( - "Healthy" to response.healthy.toString() - ) - ) - } - - // Neue Reitsport-Authentication-Sektion - Spacer(modifier = Modifier.height(24.dp)) - - ReitsportTestingSection( - viewModel = viewModel, - uiState = uiState - ) + Button( + onClick = { viewModel.performHealthCheck() }, + enabled = !uiState.isLoading, + modifier = Modifier.weight(1f) + ) { + Text("Health Check") + } } + + // Loading indicator + if (uiState.isLoading) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + // Error message + uiState.errorMessage?.let { error -> + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "Error", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onErrorContainer, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = error, + color = MaterialTheme.colorScheme.onErrorContainer + ) + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = { viewModel.clearError() } + ) { + Text("Dismiss") + } + } + } + } + + // Simple Ping Response + uiState.simplePingResponse?.let { response -> + ResponseCard( + title = "Simple Ping Response", + status = response.status, + timestamp = response.timestamp, + service = response.service + ) + } + + // Enhanced Ping Response + uiState.enhancedPingResponse?.let { response -> + ResponseCard( + title = "Enhanced Ping Response", + status = response.status, + timestamp = response.timestamp, + service = response.service, + additionalInfo = mapOf( + "Circuit Breaker State" to response.circuitBreakerState, + "Response Time" to "${response.responseTime}ms" + ) + ) + } + + // Health Response + uiState.healthResponse?.let { response -> + ResponseCard( + title = "Health Check Response", + status = response.status, + timestamp = response.timestamp, + service = response.service, + additionalInfo = mapOf( + "Healthy" to response.healthy.toString() + ) + ) + } + + // Neue Reitsport-Authentication-Sektion + Spacer(modifier = Modifier.height(24.dp)) + + ReitsportTestingSection( + viewModel = viewModel, + uiState = uiState + ) + } } @Composable private fun ResponseCard( - title: String, - status: String, - timestamp: String, - service: String, - additionalInfo: Map = emptyMap() + title: String, + status: String, + timestamp: String, + service: String, + additionalInfo: Map = emptyMap() ) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) - InfoRow("Status", status) - InfoRow("Timestamp", timestamp) - InfoRow("Service", service) + InfoRow("Status", status) + InfoRow("Timestamp", timestamp) + InfoRow("Service", service) - additionalInfo.forEach { (key, value) -> - InfoRow(key, value) - } - } + additionalInfo.forEach { (key, value) -> + InfoRow(key, value) + } } + } } @Composable private fun InfoRow(label: String, value: String) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = "$label:", - fontWeight = FontWeight.Medium - ) - Text(text = value) - } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "$label:", + fontWeight = FontWeight.Medium + ) + Text(text = value) + } } @Composable private fun ReitsportTestingSection( - viewModel: PingViewModel, - uiState: PingUiState + viewModel: PingViewModel, + uiState: PingUiState ) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ) + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - // Header - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "🐎", - style = MaterialTheme.typography.headlineMedium - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Reitsport-Authentication-Testing", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold - ) - } + // Header + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "🐎", + style = MaterialTheme.typography.headlineMedium + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Reitsport-Authentication-Testing", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + } - Text( - text = "Teste verschiedene Benutzerrollen und ihre Berechtigungen im Meldestelle_Pro System", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f) - ) + Text( + text = "Teste verschiedene Benutzerrollen und ihre Berechtigungen im Meldestelle_Pro System", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f) + ) - // Rollen-Grid - LazyVerticalGrid( - columns = GridCells.Adaptive(minSize = 120.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.height(200.dp) // Feste Höhe für 2 Reihen - ) { - items(ReitsportRoles.ALL_ROLES) { role -> - RoleTestButton( - role = role, - onClick = { viewModel.testReitsportRole(role) }, - isLoading = uiState.isLoading - ) - } - } + // Rollen-Grid + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 120.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.height(200.dp) // Feste Höhe für 2 Reihen + ) { + items(ReitsportRoles.ALL_ROLES) { role -> + RoleTestButton( + role = role, + onClick = { viewModel.testReitsportRole(role) }, + isLoading = uiState.isLoading + ) } + } } + } } @Composable private fun RoleTestButton( - role: ReitsportRole, - onClick: () -> Unit, - isLoading: Boolean + role: ReitsportRole, + onClick: () -> Unit, + isLoading: Boolean ) { - OutlinedButton( - onClick = onClick, - enabled = !isLoading, - modifier = Modifier - .fillMaxWidth() - .height(80.dp), - colors = ButtonDefaults.outlinedButtonColors( - containerColor = Color.Transparent, - contentColor = when (role.category) { - RoleCategory.SYSTEM -> Color(0xFFFF5722) - RoleCategory.OFFICIAL -> Color(0xFF3F51B5) - RoleCategory.ACTIVE -> Color(0xFF4CAF50) - RoleCategory.PASSIVE -> Color(0xFF9E9E9E) - } - ) + OutlinedButton( + onClick = onClick, + enabled = !isLoading, + modifier = Modifier + .fillMaxWidth() + .height(80.dp), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = Color.Transparent, + contentColor = when (role.category) { + RoleCategory.SYSTEM -> Color(0xFFFF5722) + RoleCategory.OFFICIAL -> Color(0xFF3F51B5) + RoleCategory.ACTIVE -> Color(0xFF4CAF50) + RoleCategory.PASSIVE -> Color(0xFF9E9E9E) + } + ) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = role.icon, - fontSize = 20.sp - ) - Text( - text = role.displayName.split(" ").first(), // Erstes Wort nur - fontSize = 10.sp, - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center, - maxLines = 1 - ) - Text( - text = "${role.permissions.size} Rechte", - fontSize = 8.sp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), - textAlign = TextAlign.Center - ) - } + Text( + text = role.icon, + fontSize = 20.sp + ) + Text( + text = role.displayName.split(" ").first(), // Erstes Wort nur + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + maxLines = 1 + ) + Text( + text = "${role.permissions.size} Rechte", + fontSize = 8.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + textAlign = TextAlign.Center + ) } + } } diff --git a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingViewModel.kt b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingViewModel.kt index 3cd61d90..60eaf2c4 100644 --- a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingViewModel.kt +++ b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingViewModel.kt @@ -15,136 +15,136 @@ import at.mocode.ping.api.PingResponse import kotlinx.coroutines.launch data class PingUiState( - val isLoading: Boolean = false, - val simplePingResponse: PingResponse? = null, - val enhancedPingResponse: EnhancedPingResponse? = null, - val healthResponse: HealthResponse? = null, - val errorMessage: String? = null + val isLoading: Boolean = false, + val simplePingResponse: PingResponse? = null, + val enhancedPingResponse: EnhancedPingResponse? = null, + val healthResponse: HealthResponse? = null, + val errorMessage: String? = null ) class PingViewModel( - private val apiClient: PingApi = PingApiClient() + private val apiClient: PingApi = PingApiClient() ) : ViewModel() { - var uiState by mutableStateOf(PingUiState()) - private set + var uiState by mutableStateOf(PingUiState()) + private set - fun performSimplePing() { - viewModelScope.launch { - uiState = uiState.copy(isLoading = true, errorMessage = null) - try { - val response = apiClient.simplePing() - uiState = uiState.copy( - isLoading = false, - simplePingResponse = response - ) - } catch (e: Exception) { - uiState = uiState.copy( - isLoading = false, - errorMessage = "Simple ping failed: ${e.message}" - ) - } - } - } - - fun performEnhancedPing(simulate: Boolean = false) { - viewModelScope.launch { - uiState = uiState.copy(isLoading = true, errorMessage = null) - try { - val response = apiClient.enhancedPing(simulate) - uiState = uiState.copy( - isLoading = false, - enhancedPingResponse = response - ) - } catch (e: Exception) { - uiState = uiState.copy( - isLoading = false, - errorMessage = "Enhanced ping failed: ${e.message}" - ) - } - } - } - - fun performHealthCheck() { - viewModelScope.launch { - uiState = uiState.copy(isLoading = true, errorMessage = null) - try { - val response = apiClient.healthCheck() - uiState = uiState.copy( - isLoading = false, - healthResponse = response - ) - } catch (e: Exception) { - uiState = uiState.copy( - isLoading = false, - errorMessage = "Health check failed: ${e.message}" - ) - } - } - } - - fun clearError() { - uiState = uiState.copy(errorMessage = null) - } - - /** - * Erweiterte Methode: Echte API-Tests für Reitsport-Rollen - */ - fun testReitsportRole(role: ReitsportRole) { - viewModelScope.launch { - uiState = uiState.copy( - isLoading = true, - errorMessage = null - ) - - try { - // Echte API-Tests durchführen - val apiClient = ReitsportTestApi() - val testResults = apiClient.testRole(role) - - // Erfolgs-Statistiken berechnen - val successful = testResults.count { it.success } - val total = testResults.size - val successRate = if (total > 0) (successful * 100 / total) else 0 - - // Test-Summary erstellen - val summary = buildString { - appendLine("🎯 ${role.displayName} - Test Abgeschlossen") - appendLine("📊 Erfolgsrate: $successful/$total Tests ($successRate%)") - appendLine("⏱️ Durchschnittsdauer: ${testResults.map { it.duration }.average().toInt()}ms") - appendLine("🔑 Berechtigungen: ${role.permissions.size}") - appendLine("") - appendLine("📋 Test-Ergebnisse:") - - testResults.forEach { result -> - val icon = if (result.success) "✅" else "❌" - val status = if (result.responseCode != null) " (${result.responseCode})" else "" - appendLine("$icon ${result.scenarioName}$status - ${result.duration}ms") - } - } - - // Mock-Response für Anzeige - val mockResponse = PingResponse( - status = summary, - timestamp = DateTimeHelper.formatDateTime(DateTimeHelper.now()), - service = "Reitsport-Auth-Test" - ) - - uiState = uiState.copy( - isLoading = false, - simplePingResponse = mockResponse - ) - - println("[DEBUG] Reitsport-API-Test: ${role.displayName}") - println("[DEBUG] Ergebnisse: $successful/$total erfolgreich") - - } catch (e: Exception) { - uiState = uiState.copy( - isLoading = false, - errorMessage = "Reitsport-API-Test fehlgeschlagen: ${e.message}" - ) - println("[ERROR] Reitsport-Test-Fehler: ${e.message}") - } + fun performSimplePing() { + viewModelScope.launch { + uiState = uiState.copy(isLoading = true, errorMessage = null) + try { + val response = apiClient.simplePing() + uiState = uiState.copy( + isLoading = false, + simplePingResponse = response + ) + } catch (e: Exception) { + uiState = uiState.copy( + isLoading = false, + errorMessage = "Simple ping failed: ${e.message}" + ) + } + } + } + + fun performEnhancedPing(simulate: Boolean = false) { + viewModelScope.launch { + uiState = uiState.copy(isLoading = true, errorMessage = null) + try { + val response = apiClient.enhancedPing(simulate) + uiState = uiState.copy( + isLoading = false, + enhancedPingResponse = response + ) + } catch (e: Exception) { + uiState = uiState.copy( + isLoading = false, + errorMessage = "Enhanced ping failed: ${e.message}" + ) + } + } + } + + fun performHealthCheck() { + viewModelScope.launch { + uiState = uiState.copy(isLoading = true, errorMessage = null) + try { + val response = apiClient.healthCheck() + uiState = uiState.copy( + isLoading = false, + healthResponse = response + ) + } catch (e: Exception) { + uiState = uiState.copy( + isLoading = false, + errorMessage = "Health check failed: ${e.message}" + ) + } + } + } + + fun clearError() { + uiState = uiState.copy(errorMessage = null) + } + + /** + * Erweiterte Methode: Echte API-Tests für Reitsport-Rollen + */ + fun testReitsportRole(role: ReitsportRole) { + viewModelScope.launch { + uiState = uiState.copy( + isLoading = true, + errorMessage = null + ) + + try { + // Echte API-Tests durchführen + val apiClient = ReitsportTestApi() + val testResults = apiClient.testRole(role) + + // Erfolgs-Statistiken berechnen + val successful = testResults.count { it.success } + val total = testResults.size + val successRate = if (total > 0) (successful * 100 / total) else 0 + + // Test-Summary erstellen + val summary = buildString { + appendLine("🎯 ${role.displayName} - Test Abgeschlossen") + appendLine("📊 Erfolgsrate: $successful/$total Tests ($successRate%)") + appendLine("⏱️ Durchschnittsdauer: ${testResults.map { it.duration }.average().toInt()}ms") + appendLine("🔑 Berechtigungen: ${role.permissions.size}") + appendLine("") + appendLine("📋 Test-Ergebnisse:") + + testResults.forEach { result -> + val icon = if (result.success) "✅" else "❌" + val status = if (result.responseCode != null) " (${result.responseCode})" else "" + appendLine("$icon ${result.scenarioName}$status - ${result.duration}ms") + } } + + // Mock-Response für Anzeige + val mockResponse = PingResponse( + status = summary, + timestamp = DateTimeHelper.formatDateTime(DateTimeHelper.now()), + service = "Reitsport-Auth-Test" + ) + + uiState = uiState.copy( + isLoading = false, + simplePingResponse = mockResponse + ) + + println("[DEBUG] Reitsport-API-Test: ${role.displayName}") + println("[DEBUG] Ergebnisse: $successful/$total erfolgreich") + + } catch (e: Exception) { + uiState = uiState.copy( + isLoading = false, + errorMessage = "Reitsport-API-Test fehlgeschlagen: ${e.message}" + ) + println("[ERROR] Reitsport-Test-Fehler: ${e.message}") + } } + } } diff --git a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/api/ReitsportTestApi.kt b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/api/ReitsportTestApi.kt index 4f1bf25e..07ea9a53 100644 --- a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/api/ReitsportTestApi.kt +++ b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/api/ReitsportTestApi.kt @@ -8,254 +8,258 @@ import kotlinx.coroutines.delay /** * API-Client für Reitsport-Authentication-Testing - * Testet verschiedene Services mit rollenbasierten Tokens + * testet verschiedene Services mit rollenbasierten Tokens */ class ReitsportTestApi { - companion object { - // URLs der verfügbaren Services - private const val PING_SERVICE_URL = "http://localhost:8082" - private const val GATEWAY_URL = "http://localhost:8081" + companion object { + // URLs der verfügbaren Services + private const val PING_SERVICE_URL = "http://localhost:8082" + private const val GATEWAY_URL = "http://localhost:8081" - // Mock URLs für auskommentierte Services - private const val MEMBERS_SERVICE_URL = "http://localhost:8083" // Auskommentiert - private const val HORSES_SERVICE_URL = "http://localhost:8084" // Auskommentiert - private const val EVENTS_SERVICE_URL = "http://localhost:8085" // Auskommentiert + // Mock URLs für auskommentierte Services + private const val MEMBERS_SERVICE_URL = "http://localhost:8083" // Auskommentiert + private const val HORSES_SERVICE_URL = "http://localhost:8084" // Auskommentiert + private const val EVENTS_SERVICE_URL = "http://localhost:8085" // Auskommentiert + } + + /** + * Teste eine Rolle gegen verfügbare Services + */ + suspend fun testRole(role: ReitsportRole): List { + val results = mutableListOf() + + // 1. Test Ping-Service (immer verfügbar) + results.add(testPingService(role)) + + // 2. Test Gateway Health (immer verfügbar) + results.add(testGatewayHealth(role)) + + // 3. Test rollenspezifische Services + when (role.roleType) { + RolleE.ADMIN, RolleE.VEREINS_ADMIN -> { + results.add(testMembersService(role)) + results.add(testSystemAccess(role)) + } + + RolleE.FUNKTIONAER -> { + results.add(testEventsService(role)) + results.add(testMembersService(role)) + } + + RolleE.TIERARZT, RolleE.TRAINER -> { + results.add(testHorsesService(role)) + } + + RolleE.REITER -> { + results.add(testMembersService(role)) + } + + RolleE.RICHTER, RolleE.ZUSCHAUER, RolleE.GAST -> { + results.add(testPublicAccess(role)) + } } - /** - * Teste eine Rolle gegen verfügbare Services - */ - suspend fun testRole(role: ReitsportRole): List { - val results = mutableListOf() + return results + } - // 1. Test Ping-Service (immer verfügbar) - results.add(testPingService(role)) + /** + * Test 1: Ping-Service (verfügbar) + */ + private suspend fun testPingService(role: ReitsportRole): ApiTestResult { + val startTime = DateTimeHelper.now() - // 2. Test Gateway Health (immer verfügbar) - results.add(testGatewayHealth(role)) + return try { + // Simuliere HTTP-Call zum Ping-Service + delay(200) - // 3. Test rollenspezifische Services - when (role.roleType) { - RolleE.ADMIN, RolleE.VEREINS_ADMIN -> { - results.add(testMembersService(role)) - results.add(testSystemAccess(role)) - } - RolleE.FUNKTIONAER -> { - results.add(testEventsService(role)) - results.add(testMembersService(role)) - } - RolleE.TIERARZT, RolleE.TRAINER -> { - results.add(testHorsesService(role)) - } - RolleE.REITER -> { - results.add(testMembersService(role)) - } - RolleE.RICHTER, RolleE.ZUSCHAUER, RolleE.GAST -> { - results.add(testPublicAccess(role)) - } - } + val duration = DateTimeHelper.now() - startTime + val endpoint = "$PING_SERVICE_URL/health" - return results + ApiTestResult( + scenarioId = "ping-health", + scenarioName = "Ping Service Health", + endpoint = endpoint, + method = "GET", + expectedResult = "Service erreichbar", + actualResult = "✅ Ping-Service läuft (HTTP 200)", + success = true, + responseCode = 200, + duration = duration, + token = generateMockToken(role), + responseData = """{"status":"pong","service":"ping-service","healthy":true}""" + ) + } catch (e: Exception) { + ApiTestResult( + scenarioId = "ping-health", + scenarioName = "Ping Service Health", + endpoint = "$PING_SERVICE_URL/health", + method = "GET", + expectedResult = "Service erreichbar", + actualResult = "❌ Fehler: ${e.message}", + success = false, + duration = DateTimeHelper.now() - startTime, + errorMessage = e.message + ) } + } - /** - * Test 1: Ping-Service (verfügbar) - */ - private suspend fun testPingService(role: ReitsportRole): ApiTestResult { - val startTime = DateTimeHelper.now() + /** + * Test 2: Gateway Health (verfügbar) + */ + private suspend fun testGatewayHealth(role: ReitsportRole): ApiTestResult { + val startTime = DateTimeHelper.now() - return try { - // Simuliere HTTP-Call zum Ping-Service - delay(200) + return try { + delay(150) - val duration = DateTimeHelper.now() - startTime - val endpoint = "$PING_SERVICE_URL/health" + val duration = DateTimeHelper.now() - startTime + val endpoint = "$GATEWAY_URL/actuator/health" - ApiTestResult( - scenarioId = "ping-health", - scenarioName = "Ping Service Health", - endpoint = endpoint, - method = "GET", - expectedResult = "Service erreichbar", - actualResult = "✅ Ping-Service läuft (HTTP 200)", - success = true, - responseCode = 200, - duration = duration, - token = generateMockToken(role), - responseData = """{"status":"pong","service":"ping-service","healthy":true}""" - ) - } catch (e: Exception) { - ApiTestResult( - scenarioId = "ping-health", - scenarioName = "Ping Service Health", - endpoint = "$PING_SERVICE_URL/health", - method = "GET", - expectedResult = "Service erreichbar", - actualResult = "❌ Fehler: ${e.message}", - success = false, - duration = DateTimeHelper.now() - startTime, - errorMessage = e.message - ) - } + ApiTestResult( + scenarioId = "gateway-health", + scenarioName = "API Gateway Health", + endpoint = endpoint, + method = "GET", + expectedResult = "Gateway gesund", + actualResult = "✅ Gateway erreichbar, Service Discovery aktiv", + success = true, + responseCode = 200, + duration = duration, + token = generateMockToken(role), + responseData = """{"status":"UP","components":{"consul":{"status":"UP"}}}""" + ) + } catch (e: Exception) { + ApiTestResult( + scenarioId = "gateway-health", + scenarioName = "API Gateway Health", + endpoint = "$GATEWAY_URL/actuator/health", + method = "GET", + expectedResult = "Gateway gesund", + actualResult = "❌ Gateway nicht erreichbar: ${e.message}", + success = false, + duration = DateTimeHelper.now() - startTime, + errorMessage = e.message + ) } + } - /** - * Test 2: Gateway Health (verfügbar) - */ - private suspend fun testGatewayHealth(role: ReitsportRole): ApiTestResult { - val startTime = DateTimeHelper.now() + /** + * Test 3: Members-Service (auskommentiert - Graceful Degradation) + */ + private suspend fun testMembersService(role: ReitsportRole): ApiTestResult { + val startTime = DateTimeHelper.now() + delay(100) - return try { - delay(150) + return ApiTestResult( + scenarioId = "members-unavailable", + scenarioName = "Members Service", + endpoint = "$MEMBERS_SERVICE_URL/api/members", + method = "GET", + expectedResult = "Mitglieder-Daten abrufen", + actualResult = "⚠️ Service temporär deaktiviert (in settings.gradle.kts auskommentiert)", + success = false, + responseCode = 503, // Service Unavailable + duration = DateTimeHelper.now() - startTime, + token = generateMockToken(role), + errorMessage = "Service ist in der aktuellen Konfiguration nicht verfügbar" + ) + } - val duration = DateTimeHelper.now() - startTime - val endpoint = "$GATEWAY_URL/actuator/health" + /** + * Test 4: Horses-Service (auskommentiert) + */ + private suspend fun testHorsesService(role: ReitsportRole): ApiTestResult { + val startTime = DateTimeHelper.now() + delay(100) - ApiTestResult( - scenarioId = "gateway-health", - scenarioName = "API Gateway Health", - endpoint = endpoint, - method = "GET", - expectedResult = "Gateway gesund", - actualResult = "✅ Gateway erreichbar, Service Discovery aktiv", - success = true, - responseCode = 200, - duration = duration, - token = generateMockToken(role), - responseData = """{"status":"UP","components":{"consul":{"status":"UP"}}}""" - ) - } catch (e: Exception) { - ApiTestResult( - scenarioId = "gateway-health", - scenarioName = "API Gateway Health", - endpoint = "$GATEWAY_URL/actuator/health", - method = "GET", - expectedResult = "Gateway gesund", - actualResult = "❌ Gateway nicht erreichbar: ${e.message}", - success = false, - duration = DateTimeHelper.now() - startTime, - errorMessage = e.message - ) - } - } + return ApiTestResult( + scenarioId = "horses-unavailable", + scenarioName = "Horses Service", + endpoint = "$HORSES_SERVICE_URL/api/horses", + method = "GET", + expectedResult = "Pferde-Daten abrufen", + actualResult = "⚠️ Service temporär deaktiviert (in settings.gradle.kts auskommentiert)", + success = false, + responseCode = 503, + duration = DateTimeHelper.now() - startTime, + token = generateMockToken(role), + errorMessage = "Service wird später aktiviert" + ) + } - /** - * Test 3: Members-Service (auskommentiert - Graceful Degradation) - */ - private suspend fun testMembersService(role: ReitsportRole): ApiTestResult { - val startTime = DateTimeHelper.now() - delay(100) + /** + * Test 5: Events-Service (auskommentiert) + */ + private suspend fun testEventsService(role: ReitsportRole): ApiTestResult { + val startTime = DateTimeHelper.now() + delay(100) - return ApiTestResult( - scenarioId = "members-unavailable", - scenarioName = "Members Service", - endpoint = "$MEMBERS_SERVICE_URL/api/members", - method = "GET", - expectedResult = "Mitglieder-Daten abrufen", - actualResult = "⚠️ Service temporär deaktiviert (in settings.gradle.kts auskommentiert)", - success = false, - responseCode = 503, // Service Unavailable - duration = DateTimeHelper.now() - startTime, - token = generateMockToken(role), - errorMessage = "Service ist in der aktuellen Konfiguration nicht verfügbar" - ) - } + return ApiTestResult( + scenarioId = "events-unavailable", + scenarioName = "Events Service", + endpoint = "$EVENTS_SERVICE_URL/api/events", + method = "GET", + expectedResult = "Veranstaltungs-Daten abrufen", + actualResult = "⚠️ Service temporär deaktiviert (in settings.gradle.kts auskommentiert)", + success = false, + responseCode = 503, + duration = DateTimeHelper.now() - startTime, + token = generateMockToken(role), + errorMessage = "Service in Entwicklung" + ) + } - /** - * Test 4: Horses-Service (auskommentiert) - */ - private suspend fun testHorsesService(role: ReitsportRole): ApiTestResult { - val startTime = DateTimeHelper.now() - delay(100) + /** + * Test 6: System-Zugriff (für Admins) + */ + private suspend fun testSystemAccess(role: ReitsportRole): ApiTestResult { + val startTime = DateTimeHelper.now() + delay(300) - return ApiTestResult( - scenarioId = "horses-unavailable", - scenarioName = "Horses Service", - endpoint = "$HORSES_SERVICE_URL/api/horses", - method = "GET", - expectedResult = "Pferde-Daten abrufen", - actualResult = "⚠️ Service temporär deaktiviert (in settings.gradle.kts auskommentiert)", - success = false, - responseCode = 503, - duration = DateTimeHelper.now() - startTime, - token = generateMockToken(role), - errorMessage = "Service wird später aktiviert" - ) - } + val hasSystemAccess = role.roleType == RolleE.ADMIN - /** - * Test 5: Events-Service (auskommentiert) - */ - private suspend fun testEventsService(role: ReitsportRole): ApiTestResult { - val startTime = DateTimeHelper.now() - delay(100) + return ApiTestResult( + scenarioId = "system-access", + scenarioName = "System-Administration", + endpoint = "$GATEWAY_URL/actuator/info", + method = "GET", + expectedResult = if (hasSystemAccess) "System-Info verfügbar" else "Zugriff verweigert", + actualResult = if (hasSystemAccess) "✅ System-Informationen zugänglich" else "❌ Insufficient permissions", + success = hasSystemAccess, + responseCode = if (hasSystemAccess) 200 else 403, + duration = DateTimeHelper.now() - startTime, + token = generateMockToken(role) + ) + } - return ApiTestResult( - scenarioId = "events-unavailable", - scenarioName = "Events Service", - endpoint = "$EVENTS_SERVICE_URL/api/events", - method = "GET", - expectedResult = "Veranstaltungs-Daten abrufen", - actualResult = "⚠️ Service temporär deaktiviert (in settings.gradle.kts auskommentiert)", - success = false, - responseCode = 503, - duration = DateTimeHelper.now() - startTime, - token = generateMockToken(role), - errorMessage = "Service in Entwicklung" - ) - } + /** + * Test 7: Öffentlicher Zugriff + */ + private suspend fun testPublicAccess(role: ReitsportRole): ApiTestResult { + val startTime = DateTimeHelper.now() + delay(150) - /** - * Test 6: System-Zugriff (für Admins) - */ - private suspend fun testSystemAccess(role: ReitsportRole): ApiTestResult { - val startTime = DateTimeHelper.now() - delay(300) + return ApiTestResult( + scenarioId = "public-access", + scenarioName = "Öffentliche Informationen", + endpoint = "$GATEWAY_URL/api/public/info", + method = "GET", + expectedResult = "Öffentliche Daten verfügbar", + actualResult = "✅ Öffentliche Informationen zugänglich (kein Token erforderlich)", + success = true, + responseCode = 200, + duration = DateTimeHelper.now() - startTime, + token = null // Kein Token für öffentlichen Zugriff + ) + } - val hasSystemAccess = role.roleType == RolleE.ADMIN - - return ApiTestResult( - scenarioId = "system-access", - scenarioName = "System-Administration", - endpoint = "$GATEWAY_URL/actuator/info", - method = "GET", - expectedResult = if (hasSystemAccess) "System-Info verfügbar" else "Zugriff verweigert", - actualResult = if (hasSystemAccess) "✅ System-Informationen zugänglich" else "❌ Insufficient permissions", - success = hasSystemAccess, - responseCode = if (hasSystemAccess) 200 else 403, - duration = DateTimeHelper.now() - startTime, - token = generateMockToken(role) - ) - } - - /** - * Test 7: Öffentlicher Zugriff - */ - private suspend fun testPublicAccess(role: ReitsportRole): ApiTestResult { - val startTime = DateTimeHelper.now() - delay(150) - - return ApiTestResult( - scenarioId = "public-access", - scenarioName = "Öffentliche Informationen", - endpoint = "$GATEWAY_URL/api/public/info", - method = "GET", - expectedResult = "Öffentliche Daten verfügbar", - actualResult = "✅ Öffentliche Informationen zugänglich (kein Token erforderlich)", - success = true, - responseCode = 200, - duration = DateTimeHelper.now() - startTime, - token = null // Kein Token für öffentlichen Zugriff - ) - } - - /** - * Generiere Mock-Token für Tests - */ - private fun generateMockToken(role: ReitsportRole): String { - // Phase 3: Mock-Token (später echte Keycloak-Integration) - val mockPayload = """{"role":"${role.roleType}","permissions":${role.permissions.size}}""" - return "mock.token.${DateTimeHelper.now()}.${role.roleType}" - } + /** + * Generiere Mock-Token für Tests + */ + private fun generateMockToken(role: ReitsportRole): String { + // Phase 3: Mock-Token (später echte Keycloak-Integration) + val mockPayload = """{"role":"${role.roleType}","permissions":${role.permissions.size}}""" + return "mock.token.${DateTimeHelper.now()}.${role.roleType}" + } } diff --git a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/AuthEnums.kt b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/AuthEnums.kt index ecf843b6..f527d7ef 100644 --- a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/AuthEnums.kt +++ b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/AuthEnums.kt @@ -8,15 +8,15 @@ import kotlinx.serialization.Serializable */ @Serializable enum class RolleE { - ADMIN, // System administrator - VEREINS_ADMIN, // Club administrator - FUNKTIONAER, // Official/functionary - REITER, // Rider - TRAINER, // Trainer - RICHTER, // Judge - TIERARZT, // Veterinarian - ZUSCHAUER, // Spectator - GAST // Guest + ADMIN, // System administrator + VEREINS_ADMIN, // Club administrator + FUNKTIONAER, // Official/functionary + REITER, // Rider + TRAINER, // Trainer + RICHTER, // Judge + TIERARZT, // Veterinarian + ZUSCHAUER, // Spectator + GAST // Guest } /** @@ -25,27 +25,27 @@ enum class RolleE { */ @Serializable enum class BerechtigungE { - // Person management - PERSON_READ, - PERSON_CREATE, - PERSON_UPDATE, - PERSON_DELETE, + // Person management + PERSON_READ, + PERSON_CREATE, + PERSON_UPDATE, + PERSON_DELETE, - // Club management - VEREIN_READ, - VEREIN_CREATE, - VEREIN_UPDATE, - VEREIN_DELETE, + // Club management + VEREIN_READ, + VEREIN_CREATE, + VEREIN_UPDATE, + VEREIN_DELETE, - // Event management - VERANSTALTUNG_READ, - VERANSTALTUNG_CREATE, - VERANSTALTUNG_UPDATE, - VERANSTALTUNG_DELETE, + // Event management + VERANSTALTUNG_READ, + VERANSTALTUNG_CREATE, + VERANSTALTUNG_UPDATE, + VERANSTALTUNG_DELETE, - // Horse management - PFERD_READ, - PFERD_CREATE, - PFERD_UPDATE, - PFERD_DELETE + // Horse management + PFERD_READ, + PFERD_CREATE, + PFERD_UPDATE, + PFERD_DELETE } diff --git a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/Phase1Validation.kt b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/Phase1Validation.kt deleted file mode 100644 index 467a9276..00000000 --- a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/Phase1Validation.kt +++ /dev/null @@ -1,93 +0,0 @@ -package at.mocode.clients.pingfeature.model - -/** - * Phase 1 Validierung für Reitsport-Authentication-Testing - * Testet alle Erfolgs-Kriterien aus der Aufgabenstellung - */ -object Phase1Validation { - - /** - * Führt alle Phase 1 Validierungen durch - */ - fun validatePhase1(): String { - val results = mutableListOf() - - // ✅ Test 1: Anzahl Rollen (erwartet: 9) - val roleCount = ReitsportRoles.ALL_ROLES.size - results.add("✅ Rollen-Anzahl: $roleCount (erwartet: 9) - ${if (roleCount == 9) "ERFOLG" else "FEHLER"}") - - // ✅ Test 2: Admin-Rolle verfügbar - val adminRole = ReitsportRoles.ADMIN - results.add("✅ Admin-Rolle: ${adminRole.displayName} - ERFOLG") - - // ✅ Test 3: Alle Kategorien verfügbar - val categories = ReitsportRoles.ROLES_BY_CATEGORY.keys - results.add("✅ Kategorien: $categories - ERFOLG") - results.add(" - SYSTEM: ${ReitsportRoles.ROLES_BY_CATEGORY[RoleCategory.SYSTEM]?.size ?: 0} Rollen") - results.add(" - OFFICIAL: ${ReitsportRoles.ROLES_BY_CATEGORY[RoleCategory.OFFICIAL]?.size ?: 0} Rollen") - results.add(" - ACTIVE: ${ReitsportRoles.ROLES_BY_CATEGORY[RoleCategory.ACTIVE]?.size ?: 0} Rollen") - results.add(" - PASSIVE: ${ReitsportRoles.ROLES_BY_CATEGORY[RoleCategory.PASSIVE]?.size ?: 0} Rollen") - - // ✅ Test 4: DateTime funktioniert - val timestamp = DateTimeHelper.now() - results.add("✅ DateTime funktioniert: $timestamp - ERFOLG") - - // ✅ Test 5: Test-ID generiert - val testId = getTimeMillis().toString() - results.add("✅ Test-ID generiert: $testId - ERFOLG") - - // ✅ Test 6: Enum-Zugriff funktioniert - results.add("✅ RolleE Enum: ${RolleE.entries.size} Einträge - ERFOLG") - results.add("✅ BerechtigungE Enum: ${BerechtigungE.entries.size} Einträge - ERFOLG") - - // ✅ Test 7: Alle 9 Rollen einzeln prüfen - results.add("✅ Alle Rollen-Definitionen:") - ReitsportRoles.ALL_ROLES.forEachIndexed { index, role -> - results.add(" ${index + 1}. ${role.displayName} (${role.roleType}) - ${role.permissions.size} Berechtigungen") - } - - // ✅ Test 8: Berechtigungen-Zuordnung testen - val adminPermissions = ReitsportRoles.ADMIN.permissions.size - val guestPermissions = ReitsportRoles.GAST.permissions.size - results.add("✅ Admin-Berechtigungen: $adminPermissions (max)") - results.add("✅ Gast-Berechtigungen: $guestPermissions (min)") - - // ✅ Test 9: Hilfsfunktionen testen - val roleByType = ReitsportRoles.getRoleByType(RolleE.RICHTER) - results.add("✅ Rolle per Type: ${roleByType?.displayName} - ERFOLG") - - val rolesWithRead = ReitsportRoles.getRolesWithPermission(BerechtigungE.PERSON_READ) - results.add("✅ Rollen mit PERSON_READ: ${rolesWithRead.size} - ERFOLG") - - return results.joinToString("\n") - } - - /** - * Führt Performance-Test durch - */ - fun performanceTest(): String { - val start = DateTimeHelper.now() - - // Simuliere mehrere Rollen-Abfragen - repeat(100) { - ReitsportRoles.getAllRoles() - ReitsportRoles.getRoleByType(RolleE.ADMIN) - ReitsportRoles.getRolesWithPermission(BerechtigungE.PERSON_READ) - } - - val end = DateTimeHelper.now() - val duration = end - start - - return "✅ Performance-Test: $duration Zeiteinheiten für 300 Operationen - ERFOLG" - } -} - -/** - * Hilfsfunktion für externe Zeitabfrage - */ -private fun getTimeMillis(): Long = DateTimeHelper.now() - -/** - * Extension für einfacheren Zugriff - */ -private fun ReitsportRoles.getAllRoles() = ALL_ROLES diff --git a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportDomainModels.kt b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportDomainModels.kt index e6f356fd..1d573474 100644 --- a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportDomainModels.kt +++ b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportDomainModels.kt @@ -13,27 +13,27 @@ import kotlinx.serialization.Serializable */ @Serializable data class ReitsportRole( - val roleType: RolleE, - val displayName: String, - val description: String, - val icon: String, - val permissions: List, - val priority: Int, // Für Sortierung in UI (1 = höchste Priorität) - val category: RoleCategory + val roleType: RolleE, + val displayName: String, + val description: String, + val icon: String, + val permissions: List, + val priority: Int, // Für Sortierung in UI (1 = höchste Priorität) + val category: RoleCategory ) { - /** - * Hilfsfunktion: Prüft, ob diese Rolle eine bestimmte Berechtigung hat - */ - fun hasPermission(permission: BerechtigungE): Boolean { - return permissions.contains(permission) - } + /** + * Hilfsfunktion: Prüft, ob diese Rolle eine bestimmte Berechtigung hat + */ + fun hasPermission(permission: BerechtigungE): Boolean { + return permissions.contains(permission) + } - /** - * Hilfsfunktion: Gibt alle fehlenden Berechtigungen für eine Liste zurück - */ - fun getMissingPermissions(requiredPermissions: List): List { - return requiredPermissions.filter { !permissions.contains(it) } - } + /** + * Hilfsfunktion: Gibt alle fehlenden Berechtigungen für eine Liste zurück + */ + fun getMissingPermissions(requiredPermissions: List): List { + return requiredPermissions.filter { !permissions.contains(it) } + } } /** @@ -41,10 +41,10 @@ data class ReitsportRole( */ @Serializable enum class RoleCategory(val displayName: String, val color: String) { - SYSTEM("System-Verwaltung", "#FF5722"), // Rot - OFFICIAL("Offizielle Funktionen", "#3F51B5"), // Indigo - ACTIVE("Aktive Teilnahme", "#4CAF50"), // Grün - PASSIVE("Information & Zugang", "#9E9E9E") // Grau + SYSTEM("System-Verwaltung", "#FF5722"), // Rot + OFFICIAL("Offizielle Funktionen", "#3F51B5"), // Indigo + ACTIVE("Aktive Teilnahme", "#4CAF50"), // Grün + PASSIVE("Information & Zugang", "#9E9E9E") // Grau } /** @@ -52,17 +52,17 @@ enum class RoleCategory(val displayName: String, val color: String) { */ @Serializable data class AuthTestScenario( - val id: String, - val name: String, - val businessProcess: String, - val description: String, - val expectedBehavior: String, - val requiredRole: RolleE, - val requiredPermissions: List, - val testEndpoint: String, - val testMethod: String = "GET", - val priority: TestPriority = TestPriority.NORMAL, - val category: ScenarioCategory + val id: String, + val name: String, + val businessProcess: String, + val description: String, + val expectedBehavior: String, + val requiredRole: RolleE, + val requiredPermissions: List, + val testEndpoint: String, + val testMethod: String = "GET", + val priority: TestPriority = TestPriority.NORMAL, + val category: ScenarioCategory ) /** @@ -70,37 +70,37 @@ data class AuthTestScenario( */ @Serializable enum class ScenarioCategory(val displayName: String, val icon: String) { - // Kern-Geschäftsprozesse - VERANSTALTUNG_SETUP("Veranstaltungs-Einrichtung", "🏟️"), - TURNIER_MANAGEMENT("Turnier-Verwaltung", "🎪"), - BEWERB_KONFIGURATION("Bewerb-Konfiguration", "🏇"), + // Kern-Geschäftsprozesse + VERANSTALTUNG_SETUP("Veranstaltungs-Einrichtung", "🏟️"), + TURNIER_MANAGEMENT("Turnier-Verwaltung", "🎪"), + BEWERB_KONFIGURATION("Bewerb-Konfiguration", "🏇"), - // Finanzen - KASSABUCH("Kassabuch-Führung", "💰"), - ABRECHNUNG("Turnier-Abrechnung", "🧾"), + // Finanzen + KASSABUCH("Kassabuch-Führung", "💰"), + ABRECHNUNG("Turnier-Abrechnung", "🧾"), - // Nennsystem - NENNUNG_WEBFORMULAR("Nenn-Web-Formular", "📝"), - NENNUNG_MOBILE("Mobile Nennung", "📱"), - NENNTAUSCH("Nenntausch-System", "🔄"), + // Nennsystem + NENNUNG_WEBFORMULAR("Nenn-Web-Formular", "📝"), + NENNUNG_MOBILE("Mobile Nennung", "📱"), + NENNTAUSCH("Nenntausch-System", "🔄"), - // Startlisten & Zeitplan - ZEITPLAN_ERSTELLUNG("Zeitplan-Erstellung", "⏰"), - STARTERLISTE_FLEXIBEL("Flexible Starterlisten", "📋"), - RICHTER_VALIDATION("Richter-Lizenz-Validierung", "⚖️"), + // Startlisten & Zeitplan + ZEITPLAN_ERSTELLUNG("Zeitplan-Erstellung", "⏰"), + STARTERLISTE_FLEXIBEL("Flexible Starterlisten", "📋"), + RICHTER_VALIDATION("Richter-Lizenz-Validierung", "⚖️"), - // Ergebnisse - ERGEBNIS_DRESSUR("Ergebnis-Erfassung Dressur", "🎭"), - ERGEBNIS_SPRINGEN("Ergebnis-Erfassung Springen", "🚀"), - ERGEBNIS_VIELSEITIGKEIT("Ergebnis-Erfassung Vielseitigkeit", "🎯"), + // Ergebnisse + ERGEBNIS_DRESSUR("Ergebnis-Erfassung Dressur", "🎭"), + ERGEBNIS_SPRINGEN("Ergebnis-Erfassung Springen", "🚀"), + ERGEBNIS_VIELSEITIGKEIT("Ergebnis-Erfassung Vielseitigkeit", "🎯"), - // OEPS Integration - OEPS_SYNC("OEPS-Synchronisation", "🔗"), - TURNIER_NUMMER("Turnier-Nummer-Verwaltung", "🔢"), + // OEPS Integration + OEPS_SYNC("OEPS-Synchronisation", "🔗"), + TURNIER_NUMMER("Turnier-Nummer-Verwaltung", "🔢"), - // System - SYSTEM_ADMIN("System-Administration", "🔧"), - BENUTZER_VERWALTUNG("Benutzer-Verwaltung", "👥") + // System + SYSTEM_ADMIN("System-Administration", "🔧"), + BENUTZER_VERWALTUNG("Benutzer-Verwaltung", "👥") } /** @@ -108,29 +108,29 @@ enum class ScenarioCategory(val displayName: String, val icon: String) { */ @Serializable data class ComplexAuthTestScenario( - val id: String, - val name: String, - val businessProcess: String, - val description: String, - val subProcesses: List, // Multi-Step-Prozesse - val requiredRole: RolleE, - val requiredPermissions: List, - val testEndpoints: List, // Mehrere API-Calls - val mockData: Map = emptyMap(), - val expectedOutcome: String, - val priority: TestPriority = TestPriority.NORMAL, - val category: ScenarioCategory, - val oepsIntegrationRequired: Boolean = false + val id: String, + val name: String, + val businessProcess: String, + val description: String, + val subProcesses: List, // Multi-Step-Prozesse + val requiredRole: RolleE, + val requiredPermissions: List, + val testEndpoints: List, // Mehrere API-Calls + val mockData: Map = emptyMap(), + val expectedOutcome: String, + val priority: TestPriority = TestPriority.NORMAL, + val category: ScenarioCategory, + val oepsIntegrationRequired: Boolean = false ) @Serializable data class TestEndpoint( - val name: String, - val url: String, - val method: String = "GET", - val payload: String? = null, - val expectedResponseCode: Int = 200, - val description: String + val name: String, + val url: String, + val method: String = "GET", + val payload: String? = null, + val expectedResponseCode: Int = 200, + val description: String ) /** @@ -138,10 +138,10 @@ data class TestEndpoint( */ @Serializable enum class TestPriority(val displayName: String, val level: Int) { - CRITICAL("Kritisch", 1), - HIGH("Hoch", 2), - NORMAL("Normal", 3), - LOW("Niedrig", 4) + CRITICAL("Kritisch", 1), + HIGH("Hoch", 2), + NORMAL("Normal", 3), + LOW("Niedrig", 4) } /** @@ -149,29 +149,29 @@ enum class TestPriority(val displayName: String, val level: Int) { */ @Serializable data class ApiTestResult( - val scenarioId: String, - val scenarioName: String, - val endpoint: String, - val method: String, - val expectedResult: String, - val actualResult: String, - val success: Boolean, - val responseCode: Int? = null, - val duration: Long, // in Millisekunden - val timestamp: Long = getTimeMillis(), - val token: String? = null, // Gekürzte Token-Info für Debugging - val errorMessage: String? = null, - val responseData: String? = null + val scenarioId: String, + val scenarioName: String, + val endpoint: String, + val method: String, + val expectedResult: String, + val actualResult: String, + val success: Boolean, + val responseCode: Int? = null, + val duration: Long, // in Millisekunden + val timestamp: Long = getTimeMillis(), + val token: String? = null, // Gekürzte Token-Info für Debugging + val errorMessage: String? = null, + val responseData: String? = null ) { - /** - * Hilfsfunktion: Formatiert die Dauer für UI-Anzeige - */ - fun formatDuration(): String = "${duration}ms" + /** + * Hilfsfunktion: Formatiert die Dauer für UI-Anzeige + */ + fun formatDuration(): String = "${duration}ms" - /** - * Hilfsfunktion: Status-Icon für UI - */ - fun getStatusIcon(): String = if (success) "✅" else "❌" + /** + * Hilfsfunktion: Status-Icon für UI + */ + fun getStatusIcon(): String = if (success) "✅" else "❌" } /** @@ -179,33 +179,33 @@ data class ApiTestResult( */ @Serializable data class ReitsportTestResult( - val testId: String = getTimeMillis().toString(), - val role: ReitsportRole, - val scenarios: List, - val apiResults: List, - val startTime: Long, - val endTime: Long? = null, - val overallSuccess: Boolean = false, - val summary: TestSummary? = null + val testId: String = getTimeMillis().toString(), + val role: ReitsportRole, + val scenarios: List, + val apiResults: List, + val startTime: Long, + val endTime: Long? = null, + val overallSuccess: Boolean = false, + val summary: TestSummary? = null ) { - /** - * Berechnet die Gesamtdauer des Tests - */ - fun getTotalDuration(): Long = (endTime ?: getTimeMillis()) - startTime + /** + * Berechnet die Gesamtdauer des Tests + */ + fun getTotalDuration(): Long = (endTime ?: getTimeMillis()) - startTime - /** - * Berechnet Erfolgsrate in Prozent - */ - fun getSuccessRate(): Double { - if (apiResults.isEmpty()) return 0.0 - val successful = apiResults.count { it.success } - return (successful.toDouble() / apiResults.size) * 100 - } + /** + * Berechnet Erfolgsrate in Prozent + */ + fun getSuccessRate(): Double { + if (apiResults.isEmpty()) return 0.0 + val successful = apiResults.count { it.success } + return (successful.toDouble() / apiResults.size) * 100 + } - /** - * Gibt alle fehlgeschlagenen Tests zurück - */ - fun getFailedTests(): List = apiResults.filter { !it.success } + /** + * Gibt alle fehlgeschlagenen Tests zurück + */ + fun getFailedTests(): List = apiResults.filter { !it.success } } /** @@ -213,15 +213,15 @@ data class ReitsportTestResult( */ @Serializable data class TestSummary( - val totalTests: Int, - val successfulTests: Int, - val failedTests: Int, - val averageDuration: Long, - val criticalFailures: List = emptyList(), - val recommendations: List = emptyList() + val totalTests: Int, + val successfulTests: Int, + val failedTests: Int, + val averageDuration: Long, + val criticalFailures: List = emptyList(), + val recommendations: List = emptyList() ) { - val successRate: Double - get() = if (totalTests > 0) (successfulTests.toDouble() / totalTests) * 100 else 0.0 + val successRate: Double + get() = if (totalTests > 0) (successfulTests.toDouble() / totalTests) * 100 else 0.0 } /** @@ -229,17 +229,17 @@ data class TestSummary( */ @Serializable data class TestNennung( - val reiterId: String, - val pferdId: String, - val bewerbId: String, - val nennungsDatum: Long = getTimeMillis() + val reiterId: String, + val pferdId: String, + val bewerbId: String, + val nennungsDatum: Long = getTimeMillis() ) @Serializable data class TestStartbereitschaft( - val nennungId: String, - val confirmed: Boolean = true, - val confirmationTime: Long = getTimeMillis() + val nennungId: String, + val confirmed: Boolean = true, + val confirmationTime: Long = getTimeMillis() ) /** @@ -247,14 +247,14 @@ data class TestStartbereitschaft( * Temporäre Lösung für Phase 1 mit incrementellem Counter */ object DateTimeHelper { - private var counter = 1000000000L // Start mit einer realistischen Timestamp + private var counter = 1000000000L // Start mit einer realistischen Timestamp - fun now(): Long = counter++ + fun now(): Long = counter++ - fun formatDateTime(timestamp: Long): String { - // Einfache ISO-ähnliche Formatierung ohne kotlinx-datetime - return "Timestamp: $timestamp" // Temporäre Lösung für Phase 1 - } + fun formatDateTime(timestamp: Long): String { + // Einfache ISO-ähnliche Formatierung ohne kotlinx-datetime + return "Timestamp: $timestamp" // Temporäre Lösung für Phase 1 + } } /** diff --git a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportRoles.kt b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportRoles.kt index c4f8145f..23c7688d 100644 --- a/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportRoles.kt +++ b/clients/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportRoles.kt @@ -6,215 +6,215 @@ package at.mocode.clients.pingfeature.model */ object ReitsportRoles { - /** - * System-Administrator - Vollzugriff auf alle Bounded Contexts - */ - val ADMIN = ReitsportRole( - roleType = RolleE.ADMIN, - displayName = "System-Administrator", - description = "Vollzugriff auf alle Microservices und System-Konfiguration", - icon = "🔧", - permissions = BerechtigungE.entries, // Alle verfügbaren Berechtigungen - priority = 1, - category = RoleCategory.SYSTEM - ) + /** + * System-Administrator - Vollzugriff auf alle Bounded Contexts + */ + val ADMIN = ReitsportRole( + roleType = RolleE.ADMIN, + displayName = "System-Administrator", + description = "Vollzugriff auf alle Microservices und System-Konfiguration", + icon = "🔧", + permissions = BerechtigungE.entries, // Alle verfügbaren Berechtigungen + priority = 1, + category = RoleCategory.SYSTEM + ) - /** - * Vereins-Administrator - Vereins-Bounded-Context - */ - val VEREINS_ADMIN = ReitsportRole( - roleType = RolleE.VEREINS_ADMIN, - displayName = "Vereins-Administrator", - description = "Vereinsverwaltung und Mitglieder-Management", - icon = "🏛️", - permissions = listOf( - // Personen (Mitglieder) - BerechtigungE.PERSON_READ, - BerechtigungE.PERSON_CREATE, - BerechtigungE.PERSON_UPDATE, - BerechtigungE.PERSON_DELETE, - // Verein - BerechtigungE.VEREIN_READ, - BerechtigungE.VEREIN_UPDATE, - // Veranstaltungen organisieren - BerechtigungE.VERANSTALTUNG_READ, - BerechtigungE.VERANSTALTUNG_CREATE, - BerechtigungE.VERANSTALTUNG_UPDATE, - // Pferde (für Vereinsmitglieder) - BerechtigungE.PFERD_READ - ), - priority = 2, - category = RoleCategory.SYSTEM - ) + /** + * Vereins-Administrator - Vereins-Bounded-Context + */ + val VEREINS_ADMIN = ReitsportRole( + roleType = RolleE.VEREINS_ADMIN, + displayName = "Vereins-Administrator", + description = "Vereinsverwaltung und Mitglieder-Management", + icon = "🏛️", + permissions = listOf( + // Personen (Mitglieder) + BerechtigungE.PERSON_READ, + BerechtigungE.PERSON_CREATE, + BerechtigungE.PERSON_UPDATE, + BerechtigungE.PERSON_DELETE, + // Verein + BerechtigungE.VEREIN_READ, + BerechtigungE.VEREIN_UPDATE, + // Veranstaltungen organisieren + BerechtigungE.VERANSTALTUNG_READ, + BerechtigungE.VERANSTALTUNG_CREATE, + BerechtigungE.VERANSTALTUNG_UPDATE, + // Pferde (für Vereinsmitglieder) + BerechtigungE.PFERD_READ + ), + priority = 2, + category = RoleCategory.SYSTEM + ) - /** - * Funktionär - Event-Management-Bounded-Context - */ - val FUNKTIONAER = ReitsportRole( - roleType = RolleE.FUNKTIONAER, - displayName = "Funktionär (Meldestelle)", - description = "Turnierorganisation: Nennungen, Starterlisten, Meldestellen-Workflows", - icon = "⚖️", - permissions = listOf( - // Lesen aller relevanten Daten - BerechtigungE.PERSON_READ, - BerechtigungE.PFERD_READ, - BerechtigungE.VERANSTALTUNG_READ, - BerechtigungE.VERANSTALTUNG_UPDATE, // Turnier-Management - // Erweiterte Rechte in Veranstaltungs-Context - // (Hier werden später Nennung-, Startlisten-Berechtigungen hinzugefügt) - ), - priority = 3, - category = RoleCategory.OFFICIAL - ) + /** + * Funktionär - Event-Management-Bounded-Context + */ + val FUNKTIONAER = ReitsportRole( + roleType = RolleE.FUNKTIONAER, + displayName = "Funktionär (Meldestelle)", + description = "Turnierorganisation: Nennungen, Starterlisten, Meldestellen-Workflows", + icon = "⚖️", + permissions = listOf( + // Lesen aller relevanten Daten + BerechtigungE.PERSON_READ, + BerechtigungE.PFERD_READ, + BerechtigungE.VERANSTALTUNG_READ, + BerechtigungE.VERANSTALTUNG_UPDATE, // Turnier-Management + // Erweiterte Rechte in Veranstaltungs-Context + // (Hier werden später Nennung-, Startlisten-Berechtigungen hinzugefügt) + ), + priority = 3, + category = RoleCategory.OFFICIAL + ) - /** - * Richter - Spezialisierte Bewertungs-Rolle - */ - val RICHTER = ReitsportRole( - roleType = RolleE.RICHTER, - displayName = "Richter", - description = "Prüfungs-Bewertung und Ergebnis-Eingabe (ReadOnly-Zugriff auf Stammdaten)", - icon = "⚖️", - permissions = listOf( - // Nur Lese-Zugriff auf relevante Daten - BerechtigungE.PERSON_READ, // Starter-Info - BerechtigungE.PFERD_READ, // Pferde-Info - BerechtigungE.VERANSTALTUNG_READ // Prüfungs-Details - // Ergebnis-Eingabe wird später als eigener Bounded Context hinzugefügt - ), - priority = 4, - category = RoleCategory.OFFICIAL - ) + /** + * Richter - Spezialisierte Bewertungs-Rolle + */ + val RICHTER = ReitsportRole( + roleType = RolleE.RICHTER, + displayName = "Richter", + description = "Prüfungs-Bewertung und Ergebnis-Eingabe (ReadOnly-Zugriff auf Stammdaten)", + icon = "⚖️", + permissions = listOf( + // Nur Lese-Zugriff auf relevante Daten + BerechtigungE.PERSON_READ, // Starter-Info + BerechtigungE.PFERD_READ, // Pferde-Info + BerechtigungE.VERANSTALTUNG_READ // Prüfungs-Details + // Ergebnis-Eingabe wird später als eigener Bounded Context hinzugefügt + ), + priority = 4, + category = RoleCategory.OFFICIAL + ) - /** - * Tierarzt - Veterinär-Bounded-Context - */ - val TIERARZT = ReitsportRole( - roleType = RolleE.TIERARZT, - displayName = "Tierarzt", - description = "Veterinärkontrollen und Pferde-Gesundheits-Management", - icon = "🩺", - permissions = listOf( - BerechtigungE.PFERD_READ, - BerechtigungE.PFERD_UPDATE, // Gesundheitsdaten, Vet-Checks - BerechtigungE.PERSON_READ, // Besitzer-Kontakt - BerechtigungE.VERANSTALTUNG_READ // Turnier-Context für Kontrollen - ), - priority = 5, - category = RoleCategory.OFFICIAL - ) + /** + * Tierarzt - Veterinär-Bounded-Context + */ + val TIERARZT = ReitsportRole( + roleType = RolleE.TIERARZT, + displayName = "Tierarzt", + description = "Veterinärkontrollen und Pferde-Gesundheits-Management", + icon = "🩺", + permissions = listOf( + BerechtigungE.PFERD_READ, + BerechtigungE.PFERD_UPDATE, // Gesundheitsdaten, Vet-Checks + BerechtigungE.PERSON_READ, // Besitzer-Kontakt + BerechtigungE.VERANSTALTUNG_READ // Turnier-Context für Kontrollen + ), + priority = 5, + category = RoleCategory.OFFICIAL + ) - /** - * Trainer - Training-Bounded-Context (zukünftig) - */ - val TRAINER = ReitsportRole( - roleType = RolleE.TRAINER, - displayName = "Trainer", - description = "Schützlings-Betreuung und Training-Management", - icon = "🏃‍♂️", - permissions = listOf( - BerechtigungE.PERSON_READ, // Schützlinge - BerechtigungE.PFERD_READ, // Trainingspferde - BerechtigungE.VERANSTALTUNG_READ // Turnier-Planung für Schützlinge - // Training-spezifische Berechtigungen kommen später - ), - priority = 6, - category = RoleCategory.ACTIVE - ) + /** + * Trainer - Training-Bounded-Context (zukünftig) + */ + val TRAINER = ReitsportRole( + roleType = RolleE.TRAINER, + displayName = "Trainer", + description = "Schützlings-Betreuung und Training-Management", + icon = "🏃‍♂️", + permissions = listOf( + BerechtigungE.PERSON_READ, // Schützlinge + BerechtigungE.PFERD_READ, // Trainingspferde + BerechtigungE.VERANSTALTUNG_READ // Turnier-Planung für Schützlinge + // Training-spezifische Berechtigungen kommen später + ), + priority = 6, + category = RoleCategory.ACTIVE + ) - /** - * Reiter - Persönlicher Bounded Context - */ - val REITER = ReitsportRole( - roleType = RolleE.REITER, - displayName = "Reiter", - description = "Persönliche Daten, eigene Pferde und Turnier-Teilnahme", - icon = "🐎", - permissions = listOf( - BerechtigungE.PERSON_READ, // Nur eigene Daten - BerechtigungE.PFERD_READ, // Nur eigene Pferde - BerechtigungE.VERANSTALTUNG_READ // Öffentliche Turnier-Infos - // Eigene Daten ändern: Später als PERSON_UPDATE_OWN, PFERD_UPDATE_OWN - ), - priority = 7, - category = RoleCategory.ACTIVE - ) + /** + * Reiter - Persönlicher Bounded Context + */ + val REITER = ReitsportRole( + roleType = RolleE.REITER, + displayName = "Reiter", + description = "Persönliche Daten, eigene Pferde und Turnier-Teilnahme", + icon = "🐎", + permissions = listOf( + BerechtigungE.PERSON_READ, // Nur eigene Daten + BerechtigungE.PFERD_READ, // Nur eigene Pferde + BerechtigungE.VERANSTALTUNG_READ // Öffentliche Turnier-Infos + // Eigene Daten ändern: Später als PERSON_UPDATE_OWN, PFERD_UPDATE_OWN + ), + priority = 7, + category = RoleCategory.ACTIVE + ) - /** - * Zuschauer - Public-Read-Only Bounded Context - */ - val ZUSCHAUER = ReitsportRole( - roleType = RolleE.ZUSCHAUER, - displayName = "Zuschauer", - description = "Öffentliche Informationen: Starterlisten, Ergebnisse, Zeitpläne", - icon = "👁️", - permissions = listOf( - BerechtigungE.VERANSTALTUNG_READ // Nur öffentliche Turnier-Daten - // Später: STARTERLISTE_READ_PUBLIC, ERGEBNIS_READ_PUBLIC - ), - priority = 8, - category = RoleCategory.PASSIVE - ) + /** + * Zuschauer - Public-Read-Only Bounded Context + */ + val ZUSCHAUER = ReitsportRole( + roleType = RolleE.ZUSCHAUER, + displayName = "Zuschauer", + description = "Öffentliche Informationen: Starterlisten, Ergebnisse, Zeitpläne", + icon = "👁️", + permissions = listOf( + BerechtigungE.VERANSTALTUNG_READ // Nur öffentliche Turnier-Daten + // Später: STARTERLISTE_READ_PUBLIC, ERGEBNIS_READ_PUBLIC + ), + priority = 8, + category = RoleCategory.PASSIVE + ) - /** - * Gast - Keine Authentifizierung erforderlich - */ - val GAST = ReitsportRole( - roleType = RolleE.GAST, - displayName = "Gast", - description = "Öffentliche Basis-Informationen ohne Registrierung", - icon = "🔓", - permissions = emptyList(), // Nur völlig öffentliche Endpunkte - priority = 9, - category = RoleCategory.PASSIVE - ) + /** + * Gast - Keine Authentifizierung erforderlich + */ + val GAST = ReitsportRole( + roleType = RolleE.GAST, + displayName = "Gast", + description = "Öffentliche Basis-Informationen ohne Registrierung", + icon = "🔓", + permissions = emptyList(), // Nur völlig öffentliche Endpunkte + priority = 9, + category = RoleCategory.PASSIVE + ) - /** - * Alle definierten Rollen in organisatorischer Reihenfolge - */ - val ALL_ROLES = listOf( - ADMIN, - VEREINS_ADMIN, - FUNKTIONAER, - RICHTER, - TIERARZT, - TRAINER, - REITER, - ZUSCHAUER, - GAST - ) + /** + * Alle definierten Rollen in organisatorischer Reihenfolge + */ + val ALL_ROLES = listOf( + ADMIN, + VEREINS_ADMIN, + FUNKTIONAER, + RICHTER, + TIERARZT, + TRAINER, + REITER, + ZUSCHAUER, + GAST + ) - /** - * Rollen nach Bounded Context / Microservice gruppiert - */ - val ROLES_BY_BOUNDED_CONTEXT = mapOf( - "System Management" to listOf(ADMIN), - "Vereins-Service" to listOf(VEREINS_ADMIN), - "Event-Service" to listOf(FUNKTIONAER), - "Bewertungs-Service" to listOf(RICHTER), - "Vet-Service" to listOf(TIERARZT), - "Training-Service" to listOf(TRAINER), - "Member-Service" to listOf(REITER), - "Public-Service" to listOf(ZUSCHAUER, GAST) - ) + /** + * Rollen nach Bounded Context / Microservice gruppiert + */ + val ROLES_BY_BOUNDED_CONTEXT = mapOf( + "System Management" to listOf(ADMIN), + "Vereins-Service" to listOf(VEREINS_ADMIN), + "Event-Service" to listOf(FUNKTIONAER), + "Bewertungs-Service" to listOf(RICHTER), + "Vet-Service" to listOf(TIERARZT), + "Training-Service" to listOf(TRAINER), + "Member-Service" to listOf(REITER), + "Public-Service" to listOf(ZUSCHAUER, GAST) + ) - /** - * Rollen nach UI-Kategorie (für Ping-Dashboard) - */ - val ROLES_BY_CATEGORY = ALL_ROLES.groupBy { it.category } + /** + * Rollen nach UI-Kategorie (für Ping-Dashboard) + */ + val ROLES_BY_CATEGORY = ALL_ROLES.groupBy { it.category } - /** - * Hilfsfunktion: Rolle nach RolleE-Typ finden - */ - fun getRoleByType(roleType: RolleE): ReitsportRole? { - return ALL_ROLES.find { it.roleType == roleType } - } + /** + * Hilfsfunktion: Rolle nach RolleE-Typ finden + */ + fun getRoleByType(roleType: RolleE): ReitsportRole? { + return ALL_ROLES.find { it.roleType == roleType } + } - /** - * Hilfsfunktion: Alle Rollen mit einer bestimmten Berechtigung - */ - fun getRolesWithPermission(permission: BerechtigungE): List { - return ALL_ROLES.filter { it.hasPermission(permission) } - } + /** + * Hilfsfunktion: Alle Rollen mit einer bestimmten Berechtigung + */ + fun getRolesWithPermission(permission: BerechtigungE): List { + return ALL_ROLES.filter { it.hasPermission(permission) } + } } diff --git a/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingApiClientTest.kt b/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingApiClientTest.kt index 2bbc6f3b..50f06980 100644 --- a/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingApiClientTest.kt +++ b/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingApiClientTest.kt @@ -12,190 +12,190 @@ import kotlin.test.assertEquals class PingApiClientTest { - private fun createMockApiClient(mockEngine: MockEngine): PingApiClient { - return PingApiClient("http://localhost:8081") + private fun createMockApiClient(mockEngine: MockEngine): PingApiClient { + return PingApiClient("http://localhost:8081") + } + + @Test + fun `simplePing should return correct response`() = runTest { + // Given + val expectedResponse = PingResponse( + status = "OK", + timestamp = "2025-09-27T21:27:00Z", + service = "ping-service" + ) + + val mockEngine = MockEngine { request -> + assertEquals("http://localhost:8081/api/ping/simple", request.url.toString()) + assertEquals(HttpMethod.Get, request.method) + + respond( + content = Json.encodeToString(PingResponse.serializer(), expectedResponse), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) } - @Test - fun `simplePing should return correct response`() = runTest { - // Given - val expectedResponse = PingResponse( - status = "OK", - timestamp = "2025-09-27T21:27:00Z", - service = "ping-service" - ) + // When + val apiClient = PingApiClient("http://localhost:8081") + // Note: This is a limitation - we can't easily inject the mock engine + // This test demonstrates the structure but would need refactoring of PingApiClient + // to accept HttpClient as dependency for full testability + } - val mockEngine = MockEngine { request -> - assertEquals("http://localhost:8081/api/ping/simple", request.url.toString()) - assertEquals(HttpMethod.Get, request.method) + @Test + fun `enhancedPing should include simulate parameter`() = runTest { + // Given + val expectedResponse = EnhancedPingResponse( + status = "OK", + timestamp = "2025-09-27T21:27:00Z", + service = "ping-service", + circuitBreakerState = "CLOSED", + responseTime = 42L + ) - respond( - content = Json.encodeToString(PingResponse.serializer(), expectedResponse), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } + val mockEngine = MockEngine { request -> + assertEquals("http://localhost:8081/api/ping/enhanced", request.url.encodedPath) + assertEquals("true", request.url.parameters["simulate"]) + assertEquals(HttpMethod.Get, request.method) - // When - val apiClient = PingApiClient("http://localhost:8081") - // Note: This is a limitation - we can't easily inject the mock engine - // This test demonstrates the structure but would need refactoring of PingApiClient - // to accept HttpClient as dependency for full testability + respond( + content = Json.encodeToString(EnhancedPingResponse.serializer(), expectedResponse), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) } - @Test - fun `enhancedPing should include simulate parameter`() = runTest { - // Given - val expectedResponse = EnhancedPingResponse( - status = "OK", - timestamp = "2025-09-27T21:27:00Z", - service = "ping-service", - circuitBreakerState = "CLOSED", - responseTime = 42L - ) + // When - This test shows the intended structure + // val apiClient = PingApiClient(httpClient = HttpClient(mockEngine)) + // val response = apiClient.enhancedPing(simulate = true) - val mockEngine = MockEngine { request -> - assertEquals("http://localhost:8081/api/ping/enhanced", request.url.encodedPath) - assertEquals("true", request.url.parameters["simulate"]) - assertEquals(HttpMethod.Get, request.method) + // Then + // assertEquals(expectedResponse, response) + } - respond( - content = Json.encodeToString(EnhancedPingResponse.serializer(), expectedResponse), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } + @Test + fun `healthCheck should return health response`() = runTest { + // Given + val expectedResponse = HealthResponse( + status = "UP", + timestamp = "2025-09-27T21:27:00Z", + service = "ping-service", + healthy = true + ) - // When - This test shows the intended structure - // val apiClient = PingApiClient(httpClient = HttpClient(mockEngine)) - // val response = apiClient.enhancedPing(simulate = true) + val mockEngine = MockEngine { request -> + assertEquals("http://localhost:8081/api/ping/health", request.url.toString()) + assertEquals(HttpMethod.Get, request.method) - // Then - // assertEquals(expectedResponse, response) + respond( + content = Json.encodeToString(HealthResponse.serializer(), expectedResponse), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) } - @Test - fun `healthCheck should return health response`() = runTest { - // Given - val expectedResponse = HealthResponse( - status = "UP", - timestamp = "2025-09-27T21:27:00Z", - service = "ping-service", - healthy = true - ) + // When - Test structure demonstration + // val apiClient = PingApiClient(httpClient = HttpClient(mockEngine)) + // val response = apiClient.healthCheck() - val mockEngine = MockEngine { request -> - assertEquals("http://localhost:8081/api/ping/health", request.url.toString()) - assertEquals(HttpMethod.Get, request.method) + // Then + // assertEquals(expectedResponse, response) + } - respond( - content = Json.encodeToString(HealthResponse.serializer(), expectedResponse), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } - - // When - Test structure demonstration - // val apiClient = PingApiClient(httpClient = HttpClient(mockEngine)) - // val response = apiClient.healthCheck() - - // Then - // assertEquals(expectedResponse, response) + @Test + fun `API client should handle HTTP errors correctly`() = runTest { + val mockEngine = MockEngine { request -> + respond( + content = """{"error": "Internal Server Error"}""", + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) } - @Test - fun `API client should handle HTTP errors correctly`() = runTest { - val mockEngine = MockEngine { request -> - respond( - content = """{"error": "Internal Server Error"}""", - status = HttpStatusCode.InternalServerError, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } + // Test structure for error handling + // val apiClient = PingApiClient(httpClient = HttpClient(mockEngine)) + // assertFailsWith { + // apiClient.simplePing() + // } + } - // Test structure for error handling - // val apiClient = PingApiClient(httpClient = HttpClient(mockEngine)) - // assertFailsWith { - // apiClient.simplePing() - // } + @Test + fun `API client should handle network errors`() = runTest { + val mockEngine = MockEngine { request -> + throw Exception("Network unreachable") } - @Test - fun `API client should handle network errors`() = runTest { - val mockEngine = MockEngine { request -> - throw Exception("Network unreachable") - } + // Test structure for network error handling + // val apiClient = PingApiClient(httpClient = HttpClient(mockEngine)) + // assertFailsWith { + // apiClient.simplePing() + // } + } - // Test structure for network error handling - // val apiClient = PingApiClient(httpClient = HttpClient(mockEngine)) - // assertFailsWith { - // apiClient.simplePing() - // } - } + @Test + fun `JSON serialization should work correctly`() { + // Given + val pingResponse = PingResponse( + status = "OK", + timestamp = "2025-09-27T21:27:00Z", + service = "test-service" + ) - @Test - fun `JSON serialization should work correctly`() { - // Given - val pingResponse = PingResponse( - status = "OK", - timestamp = "2025-09-27T21:27:00Z", - service = "test-service" - ) + // When + val json = Json.encodeToString(PingResponse.serializer(), pingResponse) + val deserializedResponse = Json.decodeFromString(PingResponse.serializer(), json) - // When - val json = Json.encodeToString(PingResponse.serializer(), pingResponse) - val deserializedResponse = Json.decodeFromString(PingResponse.serializer(), json) + // Then + assertEquals(pingResponse, deserializedResponse) + } - // Then - assertEquals(pingResponse, deserializedResponse) - } + @Test + fun `Enhanced ping response serialization should work correctly`() { + // Given + val enhancedResponse = EnhancedPingResponse( + status = "OK", + timestamp = "2025-09-27T21:27:00Z", + service = "test-service", + circuitBreakerState = "CLOSED", + responseTime = 123L + ) - @Test - fun `Enhanced ping response serialization should work correctly`() { - // Given - val enhancedResponse = EnhancedPingResponse( - status = "OK", - timestamp = "2025-09-27T21:27:00Z", - service = "test-service", - circuitBreakerState = "CLOSED", - responseTime = 123L - ) + // When + val json = Json.encodeToString(EnhancedPingResponse.serializer(), enhancedResponse) + val deserializedResponse = Json.decodeFromString(EnhancedPingResponse.serializer(), json) - // When - val json = Json.encodeToString(EnhancedPingResponse.serializer(), enhancedResponse) - val deserializedResponse = Json.decodeFromString(EnhancedPingResponse.serializer(), json) + // Then + assertEquals(enhancedResponse, deserializedResponse) + } - // Then - assertEquals(enhancedResponse, deserializedResponse) - } + @Test + fun `Health response serialization should work correctly`() { + // Given + val healthResponse = HealthResponse( + status = "UP", + timestamp = "2025-09-27T21:27:00Z", + service = "test-service", + healthy = true + ) - @Test - fun `Health response serialization should work correctly`() { - // Given - val healthResponse = HealthResponse( - status = "UP", - timestamp = "2025-09-27T21:27:00Z", - service = "test-service", - healthy = true - ) + // When + val json = Json.encodeToString(HealthResponse.serializer(), healthResponse) + val deserializedResponse = Json.decodeFromString(HealthResponse.serializer(), json) - // When - val json = Json.encodeToString(HealthResponse.serializer(), healthResponse) - val deserializedResponse = Json.decodeFromString(HealthResponse.serializer(), json) + // Then + assertEquals(healthResponse, deserializedResponse) + } - // Then - assertEquals(healthResponse, deserializedResponse) - } - - // Note: The HTTP request tests above demonstrate the test structure but are commented out - // because the current PingApiClient implementation doesn't support dependency injection - // of HttpClient. To make these tests fully functional, PingApiClient would need to be - // refactored to accept HttpClient as a constructor parameter: - // - // class PingApiClient( - // private val baseUrl: String = "http://localhost:8081", - // private val httpClient: HttpClient = HttpClient { ... } - // ) - // - // This would enable full HTTP mocking and testing capabilities. + // Note: The HTTP request tests above demonstrate the test structure but are commented out + // because the current PingApiClient implementation doesn't support dependency injection + // of HttpClient. To make these tests fully functional, PingApiClient would need to be + // refactored to accept HttpClient as a constructor parameter: + // + // class PingApiClient( + // private val baseUrl: String = "http://localhost:8081", + // private val httpClient: HttpClient = HttpClient { ... } + // ) + // + // This would enable full HTTP mocking and testing capabilities. } diff --git a/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingViewModelTest.kt b/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingViewModelTest.kt index 52b2a3cf..6af64ac4 100644 --- a/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingViewModelTest.kt +++ b/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingViewModelTest.kt @@ -10,253 +10,253 @@ import kotlin.test.* @OptIn(ExperimentalCoroutinesApi::class) class PingViewModelTest { - private lateinit var viewModel: PingViewModel - private lateinit var testApiClient: TestPingApiClient - private val testDispatcher = StandardTestDispatcher() + private lateinit var viewModel: PingViewModel + private lateinit var testApiClient: TestPingApiClient + private val testDispatcher = StandardTestDispatcher() - @BeforeTest - fun setup() { - Dispatchers.setMain(testDispatcher) - testApiClient = TestPingApiClient() - viewModel = PingViewModel(testApiClient) + @BeforeTest + fun setup() { + Dispatchers.setMain(testDispatcher) + testApiClient = TestPingApiClient() + viewModel = PingViewModel(testApiClient) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + testApiClient.reset() + } + + @Test + fun `initial state should be empty`() { + // Given & When - initial state + val initialState = viewModel.uiState + + // Then + assertFalse(initialState.isLoading) + assertNull(initialState.simplePingResponse) + assertNull(initialState.enhancedPingResponse) + assertNull(initialState.healthResponse) + assertNull(initialState.errorMessage) + } + + @Test + fun `performSimplePing should update state with success response`() = runTest(testDispatcher) { + // Given + val expectedResponse = PingResponse( + status = "OK", + timestamp = "2025-09-27T21:27:00Z", + service = "test-service" + ) + testApiClient.simplePingResponse = expectedResponse + + // When + viewModel.performSimplePing() + testDispatcher.scheduler.advanceUntilIdle() + + // Then + val finalState = viewModel.uiState + assertFalse(finalState.isLoading) + assertEquals(expectedResponse, finalState.simplePingResponse) + assertNull(finalState.errorMessage) + assertTrue(testApiClient.simplePingCalled) + } + + @Test + fun `performSimplePing should set loading state during execution`() = runTest(testDispatcher) { + // Given + testApiClient.simulateDelay = true + testApiClient.delayMs = 100 + + // When + viewModel.performSimplePing() + testDispatcher.scheduler.advanceTimeBy(1) // Allow the coroutine to start + + // Then - should be loading during execution + assertTrue(viewModel.uiState.isLoading) + assertNull(viewModel.uiState.errorMessage) + + // When - complete the operation + testDispatcher.scheduler.advanceUntilIdle() + + // Then - should not be loading anymore + assertFalse(viewModel.uiState.isLoading) + } + + @Test + fun `performSimplePing should handle error and update state`() = runTest(testDispatcher) { + // Given + val errorMessage = "Network error" + testApiClient.shouldThrowException = true + testApiClient.exceptionMessage = errorMessage + + // When + viewModel.performSimplePing() + testDispatcher.scheduler.advanceUntilIdle() + + // Then + val finalState = viewModel.uiState + assertFalse(finalState.isLoading) + assertNull(finalState.simplePingResponse) + assertEquals("Simple ping failed: $errorMessage", finalState.errorMessage) + assertTrue(testApiClient.simplePingCalled) + } + + @Test + fun `performEnhancedPing should update state with success response`() = runTest(testDispatcher) { + // Given + val expectedResponse = EnhancedPingResponse( + status = "OK", + timestamp = "2025-09-27T21:27:00Z", + service = "test-service", + circuitBreakerState = "CLOSED", + responseTime = 42L + ) + testApiClient.enhancedPingResponse = expectedResponse + + // When + viewModel.performEnhancedPing(simulate = false) + testDispatcher.scheduler.advanceUntilIdle() + + // Then + val finalState = viewModel.uiState + assertFalse(finalState.isLoading) + assertEquals(expectedResponse, finalState.enhancedPingResponse) + assertNull(finalState.errorMessage) + assertEquals(false, testApiClient.enhancedPingCalledWith) + } + + @Test + fun `performEnhancedPing should handle simulate parameter correctly`() = runTest(testDispatcher) { + // When + viewModel.performEnhancedPing(simulate = true) + testDispatcher.scheduler.advanceUntilIdle() + + // Then + assertEquals(true, testApiClient.enhancedPingCalledWith) + } + + @Test + fun `performEnhancedPing should handle error and update state`() = runTest(testDispatcher) { + // Given + val errorMessage = "Enhanced ping error" + testApiClient.shouldThrowException = true + testApiClient.exceptionMessage = errorMessage + + // When + viewModel.performEnhancedPing() + testDispatcher.scheduler.advanceUntilIdle() + + // Then + val finalState = viewModel.uiState + assertFalse(finalState.isLoading) + assertNull(finalState.enhancedPingResponse) + assertEquals("Enhanced ping failed: $errorMessage", finalState.errorMessage) + } + + @Test + fun `performHealthCheck should update state with success response`() = runTest(testDispatcher) { + // Given + val expectedResponse = HealthResponse( + status = "UP", + timestamp = "2025-09-27T21:27:00Z", + service = "test-service", + healthy = true + ) + testApiClient.healthResponse = expectedResponse + + // When + viewModel.performHealthCheck() + testDispatcher.scheduler.advanceUntilIdle() + + // Then + val finalState = viewModel.uiState + assertFalse(finalState.isLoading) + assertEquals(expectedResponse, finalState.healthResponse) + assertNull(finalState.errorMessage) + assertTrue(testApiClient.healthCheckCalled) + } + + @Test + fun `performHealthCheck should handle error and update state`() = runTest(testDispatcher) { + // Given + val errorMessage = "Health check error" + testApiClient.shouldThrowException = true + testApiClient.exceptionMessage = errorMessage + + // When + viewModel.performHealthCheck() + testDispatcher.scheduler.advanceUntilIdle() + + // Then + val finalState = viewModel.uiState + assertFalse(finalState.isLoading) + assertNull(finalState.healthResponse) + assertEquals("Health check failed: $errorMessage", finalState.errorMessage) + } + + @Test + fun `clearError should remove error message from state`() { + // Given - set up an error state by simulating an error + testApiClient.shouldThrowException = true + runTest(testDispatcher) { + viewModel.performSimplePing() + testDispatcher.scheduler.advanceUntilIdle() } - @AfterTest - fun tearDown() { - Dispatchers.resetMain() - testApiClient.reset() - } + // Verify error is present + assertNotNull(viewModel.uiState.errorMessage) - @Test - fun `initial state should be empty`() { - // Given & When - initial state - val initialState = viewModel.uiState + // When + viewModel.clearError() - // Then - assertFalse(initialState.isLoading) - assertNull(initialState.simplePingResponse) - assertNull(initialState.enhancedPingResponse) - assertNull(initialState.healthResponse) - assertNull(initialState.errorMessage) - } + // Then + assertNull(viewModel.uiState.errorMessage) + assertFalse(viewModel.uiState.isLoading) + } - @Test - fun `performSimplePing should update state with success response`() = runTest(testDispatcher) { - // Given - val expectedResponse = PingResponse( - status = "OK", - timestamp = "2025-09-27T21:27:00Z", - service = "test-service" - ) - testApiClient.simplePingResponse = expectedResponse + @Test + fun `multiple operations should clear previous error messages`() = runTest(testDispatcher) { + // Given - first operation fails + testApiClient.shouldThrowException = true + viewModel.performSimplePing() + testDispatcher.scheduler.advanceUntilIdle() + assertNotNull(viewModel.uiState.errorMessage) - // When - viewModel.performSimplePing() - testDispatcher.scheduler.advanceUntilIdle() + // When - second operation succeeds + testApiClient.shouldThrowException = false + val successResponse = PingResponse("SUCCESS", "2025-09-27T21:27:00Z", "test-service") + testApiClient.simplePingResponse = successResponse + viewModel.performSimplePing() + testDispatcher.scheduler.advanceUntilIdle() - // Then - val finalState = viewModel.uiState - assertFalse(finalState.isLoading) - assertEquals(expectedResponse, finalState.simplePingResponse) - assertNull(finalState.errorMessage) - assertTrue(testApiClient.simplePingCalled) - } + // Then - error should be cleared + assertNull(viewModel.uiState.errorMessage) + assertEquals(successResponse, viewModel.uiState.simplePingResponse) + } - @Test - fun `performSimplePing should set loading state during execution`() = runTest(testDispatcher) { - // Given - testApiClient.simulateDelay = true - testApiClient.delayMs = 100 + @Test + fun `loading state should be false after successful operation`() = runTest(testDispatcher) { + // Given + viewModel.performSimplePing() + testDispatcher.scheduler.advanceUntilIdle() - // When - viewModel.performSimplePing() - testDispatcher.scheduler.advanceTimeBy(1) // Allow the coroutine to start + // Then + assertFalse(viewModel.uiState.isLoading) + } - // Then - should be loading during execution - assertTrue(viewModel.uiState.isLoading) - assertNull(viewModel.uiState.errorMessage) + @Test + fun `all operations should call respective API methods`() = runTest(testDispatcher) { + // When + viewModel.performSimplePing() + viewModel.performEnhancedPing(true) + viewModel.performHealthCheck() + testDispatcher.scheduler.advanceUntilIdle() - // When - complete the operation - testDispatcher.scheduler.advanceUntilIdle() - - // Then - should not be loading anymore - assertFalse(viewModel.uiState.isLoading) - } - - @Test - fun `performSimplePing should handle error and update state`() = runTest(testDispatcher) { - // Given - val errorMessage = "Network error" - testApiClient.shouldThrowException = true - testApiClient.exceptionMessage = errorMessage - - // When - viewModel.performSimplePing() - testDispatcher.scheduler.advanceUntilIdle() - - // Then - val finalState = viewModel.uiState - assertFalse(finalState.isLoading) - assertNull(finalState.simplePingResponse) - assertEquals("Simple ping failed: $errorMessage", finalState.errorMessage) - assertTrue(testApiClient.simplePingCalled) - } - - @Test - fun `performEnhancedPing should update state with success response`() = runTest(testDispatcher) { - // Given - val expectedResponse = EnhancedPingResponse( - status = "OK", - timestamp = "2025-09-27T21:27:00Z", - service = "test-service", - circuitBreakerState = "CLOSED", - responseTime = 42L - ) - testApiClient.enhancedPingResponse = expectedResponse - - // When - viewModel.performEnhancedPing(simulate = false) - testDispatcher.scheduler.advanceUntilIdle() - - // Then - val finalState = viewModel.uiState - assertFalse(finalState.isLoading) - assertEquals(expectedResponse, finalState.enhancedPingResponse) - assertNull(finalState.errorMessage) - assertEquals(false, testApiClient.enhancedPingCalledWith) - } - - @Test - fun `performEnhancedPing should handle simulate parameter correctly`() = runTest(testDispatcher) { - // When - viewModel.performEnhancedPing(simulate = true) - testDispatcher.scheduler.advanceUntilIdle() - - // Then - assertEquals(true, testApiClient.enhancedPingCalledWith) - } - - @Test - fun `performEnhancedPing should handle error and update state`() = runTest(testDispatcher) { - // Given - val errorMessage = "Enhanced ping error" - testApiClient.shouldThrowException = true - testApiClient.exceptionMessage = errorMessage - - // When - viewModel.performEnhancedPing() - testDispatcher.scheduler.advanceUntilIdle() - - // Then - val finalState = viewModel.uiState - assertFalse(finalState.isLoading) - assertNull(finalState.enhancedPingResponse) - assertEquals("Enhanced ping failed: $errorMessage", finalState.errorMessage) - } - - @Test - fun `performHealthCheck should update state with success response`() = runTest(testDispatcher) { - // Given - val expectedResponse = HealthResponse( - status = "UP", - timestamp = "2025-09-27T21:27:00Z", - service = "test-service", - healthy = true - ) - testApiClient.healthResponse = expectedResponse - - // When - viewModel.performHealthCheck() - testDispatcher.scheduler.advanceUntilIdle() - - // Then - val finalState = viewModel.uiState - assertFalse(finalState.isLoading) - assertEquals(expectedResponse, finalState.healthResponse) - assertNull(finalState.errorMessage) - assertTrue(testApiClient.healthCheckCalled) - } - - @Test - fun `performHealthCheck should handle error and update state`() = runTest(testDispatcher) { - // Given - val errorMessage = "Health check error" - testApiClient.shouldThrowException = true - testApiClient.exceptionMessage = errorMessage - - // When - viewModel.performHealthCheck() - testDispatcher.scheduler.advanceUntilIdle() - - // Then - val finalState = viewModel.uiState - assertFalse(finalState.isLoading) - assertNull(finalState.healthResponse) - assertEquals("Health check failed: $errorMessage", finalState.errorMessage) - } - - @Test - fun `clearError should remove error message from state`() { - // Given - set up an error state by simulating an error - testApiClient.shouldThrowException = true - runTest(testDispatcher) { - viewModel.performSimplePing() - testDispatcher.scheduler.advanceUntilIdle() - } - - // Verify error is present - assertNotNull(viewModel.uiState.errorMessage) - - // When - viewModel.clearError() - - // Then - assertNull(viewModel.uiState.errorMessage) - assertFalse(viewModel.uiState.isLoading) - } - - @Test - fun `multiple operations should clear previous error messages`() = runTest(testDispatcher) { - // Given - first operation fails - testApiClient.shouldThrowException = true - viewModel.performSimplePing() - testDispatcher.scheduler.advanceUntilIdle() - assertNotNull(viewModel.uiState.errorMessage) - - // When - second operation succeeds - testApiClient.shouldThrowException = false - val successResponse = PingResponse("SUCCESS", "2025-09-27T21:27:00Z", "test-service") - testApiClient.simplePingResponse = successResponse - viewModel.performSimplePing() - testDispatcher.scheduler.advanceUntilIdle() - - // Then - error should be cleared - assertNull(viewModel.uiState.errorMessage) - assertEquals(successResponse, viewModel.uiState.simplePingResponse) - } - - @Test - fun `loading state should be false after successful operation`() = runTest(testDispatcher) { - // Given - viewModel.performSimplePing() - testDispatcher.scheduler.advanceUntilIdle() - - // Then - assertFalse(viewModel.uiState.isLoading) - } - - @Test - fun `all operations should call respective API methods`() = runTest(testDispatcher) { - // When - viewModel.performSimplePing() - viewModel.performEnhancedPing(true) - viewModel.performHealthCheck() - testDispatcher.scheduler.advanceUntilIdle() - - // Then - assertTrue(testApiClient.simplePingCalled) - assertEquals(true, testApiClient.enhancedPingCalledWith) - assertTrue(testApiClient.healthCheckCalled) - assertEquals(3, testApiClient.callCount) - } + // Then + assertTrue(testApiClient.simplePingCalled) + assertEquals(true, testApiClient.enhancedPingCalledWith) + assertTrue(testApiClient.healthCheckCalled) + assertEquals(3, testApiClient.callCount) + } } diff --git a/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/TestPingApiClient.kt b/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/TestPingApiClient.kt index 10e8d3f2..3a052c60 100644 --- a/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/TestPingApiClient.kt +++ b/clients/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/TestPingApiClient.kt @@ -11,95 +11,95 @@ import at.mocode.ping.api.HealthResponse */ class TestPingApiClient : PingApi { - // Test configuration properties - var shouldThrowException = false - var exceptionMessage = "Test exception" - var simulateDelay = false - var delayMs = 100L + // Test configuration properties + var shouldThrowException = false + var exceptionMessage = "Test exception" + var simulateDelay = false + var delayMs = 100L - // Response configuration - var simplePingResponse: PingResponse? = null - var enhancedPingResponse: EnhancedPingResponse? = null - var healthResponse: HealthResponse? = null + // Response configuration + var simplePingResponse: PingResponse? = null + var enhancedPingResponse: EnhancedPingResponse? = null + var healthResponse: HealthResponse? = null - // Call tracking - var simplePingCalled = false - var enhancedPingCalledWith: Boolean? = null - var healthCheckCalled = false - var callCount = 0 + // Call tracking + var simplePingCalled = false + var enhancedPingCalledWith: Boolean? = null + var healthCheckCalled = false + var callCount = 0 - override suspend fun simplePing(): PingResponse { - simplePingCalled = true - callCount++ + override suspend fun simplePing(): PingResponse { + simplePingCalled = true + callCount++ - if (simulateDelay) { - kotlinx.coroutines.delay(delayMs) - } - - if (shouldThrowException) { - throw Exception(exceptionMessage) - } - - return simplePingResponse ?: PingResponse( - status = "OK", - timestamp = "2025-09-27T21:27:00Z", - service = "test-ping-service" - ) + if (simulateDelay) { + kotlinx.coroutines.delay(delayMs) } - override suspend fun enhancedPing(simulate: Boolean): EnhancedPingResponse { - enhancedPingCalledWith = simulate - callCount++ - - if (simulateDelay) { - kotlinx.coroutines.delay(delayMs) - } - - if (shouldThrowException) { - throw Exception(exceptionMessage) - } - - return enhancedPingResponse ?: EnhancedPingResponse( - status = "OK", - timestamp = "2025-09-27T21:27:00Z", - service = "test-ping-service", - circuitBreakerState = "CLOSED", - responseTime = 42L - ) + if (shouldThrowException) { + throw Exception(exceptionMessage) } - override suspend fun healthCheck(): HealthResponse { - healthCheckCalled = true - callCount++ + return simplePingResponse ?: PingResponse( + status = "OK", + timestamp = "2025-09-27T21:27:00Z", + service = "test-ping-service" + ) + } - if (simulateDelay) { - kotlinx.coroutines.delay(delayMs) - } + override suspend fun enhancedPing(simulate: Boolean): EnhancedPingResponse { + enhancedPingCalledWith = simulate + callCount++ - if (shouldThrowException) { - throw Exception(exceptionMessage) - } - - return healthResponse ?: HealthResponse( - status = "UP", - timestamp = "2025-09-27T21:27:00Z", - service = "test-ping-service", - healthy = true - ) + if (simulateDelay) { + kotlinx.coroutines.delay(delayMs) } - // Test utilities - fun reset() { - shouldThrowException = false - exceptionMessage = "Test exception" - simulateDelay = false - delayMs = 100L - simplePingResponse = null - enhancedPingResponse = null - healthResponse = null - simplePingCalled = false - enhancedPingCalledWith = null - healthCheckCalled = false - callCount = 0 + if (shouldThrowException) { + throw Exception(exceptionMessage) } + + return enhancedPingResponse ?: EnhancedPingResponse( + status = "OK", + timestamp = "2025-09-27T21:27:00Z", + service = "test-ping-service", + circuitBreakerState = "CLOSED", + responseTime = 42L + ) + } + + override suspend fun healthCheck(): HealthResponse { + healthCheckCalled = true + callCount++ + + if (simulateDelay) { + kotlinx.coroutines.delay(delayMs) + } + + if (shouldThrowException) { + throw Exception(exceptionMessage) + } + + return healthResponse ?: HealthResponse( + status = "UP", + timestamp = "2025-09-27T21:27:00Z", + service = "test-ping-service", + healthy = true + ) + } + + // Test utilities + fun reset() { + shouldThrowException = false + exceptionMessage = "Test exception" + simulateDelay = false + delayMs = 100L + simplePingResponse = null + enhancedPingResponse = null + healthResponse = null + simplePingCalled = false + enhancedPingCalledWith = null + healthCheckCalled = false + callCount = 0 + } } diff --git a/clients/shared/build.gradle.kts b/clients/shared/build.gradle.kts index 4ed01e52..35b6c6eb 100644 --- a/clients/shared/build.gradle.kts +++ b/clients/shared/build.gradle.kts @@ -36,7 +36,7 @@ kotlin { // ... } - // WASM, nur wenn explizit aktiviert + // WASM, nur wenn explizit aktiviert if (enableWasm) { @OptIn(ExperimentalWasmDsl::class) wasmJs { browser() } @@ -58,6 +58,11 @@ kotlin { implementation(libs.ktor.client.logging) implementation(libs.ktor.client.auth) + // Dependency Injection (Koin) + implementation(libs.koin.core) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) + // Compose für shared UI components (common) implementation(compose.runtime) implementation(compose.foundation) diff --git a/clients/shared/common-ui/build.gradle.kts b/clients/shared/common-ui/build.gradle.kts index 0b71a20b..fd136b92 100644 --- a/clients/shared/common-ui/build.gradle.kts +++ b/clients/shared/common-ui/build.gradle.kts @@ -1,57 +1,59 @@ plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.kotlinSerialization) } kotlin { - val enableWasm = providers.gradleProperty("enableWasm").orNull == "true" + val enableWasm = providers.gradleProperty("enableWasm").orNull == "true" - jvmToolchain(21) + jvmToolchain(21) - jvm() - js(IR) { - browser() - nodejs() + jvm() + js(IR) { + browser() +// nodejs() + binaries.executable() + } + + // WASM, nur wenn explizit aktiviert + if (enableWasm) { + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + } + + sourceSets { + commonMain.dependencies { + // Shared module dependency + implementation(project(":clients:shared")) + + // Compose dependencies + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + implementation(compose.components.resources) + + // Coroutines + implementation(libs.kotlinx.coroutines.core) + + // Serialization + implementation(libs.kotlinx.serialization.json) + + // DateTime + implementation(libs.kotlinx.datetime) } - // WASM, nur wenn explizit aktiviert - if (enableWasm) { - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { - browser() - } + jsMain.dependencies { + // JS-specific UI dependencies if needed } - sourceSets { - commonMain.dependencies { - // Shared module dependency - implementation(project(":clients:shared")) - - // Compose dependencies - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.ui) - implementation(compose.components.resources) - - // Coroutines - implementation(libs.kotlinx.coroutines.core) - - // Serialization - implementation(libs.kotlinx.serialization.json) - - // DateTime - implementation(libs.kotlinx.datetime) - } - - jsMain.dependencies { - // JS-specific UI dependencies if needed - } - - jvmMain.dependencies { - // JVM-specific UI dependencies if needed - } + jvmMain.dependencies { + // JVM-specific UI dependencies if needed } + } } diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppFooter.kt b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppFooter.kt index 913bcd96..acb32ae9 100644 --- a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppFooter.kt +++ b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppFooter.kt @@ -12,18 +12,18 @@ import androidx.compose.ui.unit.dp @Composable fun AppFooter() { - Box( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceVariant) - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = "© 2025 Meldestelle - Built with Kotlin Multiplatform", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - } + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "© 2025 Meldestelle - Built with Kotlin Multiplatform", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } } diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppHeader.kt b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppHeader.kt index 3f1d9303..22c3dfd0 100644 --- a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppHeader.kt +++ b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppHeader.kt @@ -7,68 +7,68 @@ import androidx.compose.ui.text.font.FontWeight @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppHeader( - title: String, - onNavigateToPing: (() -> Unit)? = null, - onNavigateToLogin: (() -> Unit)? = null, - onLogout: (() -> Unit)? = null, - isAuthenticated: Boolean = false, - username: String? = null, - userPermissions: List = emptyList() + title: String, + onNavigateToPing: (() -> Unit)? = null, + onNavigateToLogin: (() -> Unit)? = null, + onLogout: (() -> Unit)? = null, + isAuthenticated: Boolean = false, + username: String? = null, + userPermissions: List = emptyList() ) { - TopAppBar( - title = { - Text( - text = title, - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold - ) - }, - actions = { - // Ping Service button - onNavigateToPing?.let { navigateAction -> - TextButton( - onClick = navigateAction - ) { - Text("Ping Service") - } - } + TopAppBar( + title = { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + }, + actions = { + // Ping Service button + onNavigateToPing?.let { navigateAction -> + TextButton( + onClick = navigateAction + ) { + Text("Ping Service") + } + } - // Authentication buttons - if (isAuthenticated) { - // Show username with admin indicator if user has delete permissions - username?.let { user -> - val isAdmin = userPermissions.any { it.contains("DELETE") } - Text( - text = if (isAdmin) "👑 Hallo, $user (Admin)" else "Hallo, $user", - style = MaterialTheme.typography.bodyMedium, - color = if (isAdmin) - MaterialTheme.colorScheme.tertiary - else - MaterialTheme.colorScheme.onPrimaryContainer - ) - } - onLogout?.let { logoutAction -> - TextButton( - onClick = logoutAction - ) { - Text("Abmelden") - } - } - } else { - // Show login button - onNavigateToLogin?.let { loginAction -> - TextButton( - onClick = loginAction - ) { - Text("Anmelden") - } - } - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, - actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) + // Authentication buttons + if (isAuthenticated) { + // Show username with admin indicator if user has delete permissions + username?.let { user -> + val isAdmin = userPermissions.any { it.contains("DELETE") } + Text( + text = if (isAdmin) "👑 Hallo, $user (Admin)" else "Hallo, $user", + style = MaterialTheme.typography.bodyMedium, + color = if (isAdmin) + MaterialTheme.colorScheme.tertiary + else + MaterialTheme.colorScheme.onPrimaryContainer + ) + } + onLogout?.let { logoutAction -> + TextButton( + onClick = logoutAction + ) { + Text("Abmelden") + } + } + } else { + // Show login button + onNavigateToLogin?.let { loginAction -> + TextButton( + onClick = loginAction + ) { + Text("Anmelden") + } + } + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer ) + ) } diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppScaffold.kt b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppScaffold.kt index 981b4d87..411d68ea 100644 --- a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppScaffold.kt +++ b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppScaffold.kt @@ -10,19 +10,19 @@ import androidx.compose.ui.Modifier @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppScaffold( - header: @Composable () -> Unit = { - AppHeader(title = "Meldestelle") - }, - content: @Composable (PaddingValues) -> Unit, - footer: @Composable () -> Unit = { - AppFooter() - }, + header: @Composable () -> Unit = { + AppHeader(title = "Meldestelle") + }, + content: @Composable (PaddingValues) -> Unit, + footer: @Composable () -> Unit = { + AppFooter() + }, ) { - Scaffold( - topBar = header, - bottomBar = footer, - modifier = Modifier.fillMaxSize() - ) { paddingValues -> - content(paddingValues) - } + Scaffold( + topBar = header, + bottomBar = footer, + modifier = Modifier.fillMaxSize() + ) { paddingValues -> + content(paddingValues) + } } diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/LoadingIndicator.kt b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/LoadingIndicator.kt index fc715ec7..d8f0bf77 100644 --- a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/LoadingIndicator.kt +++ b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/LoadingIndicator.kt @@ -9,101 +9,101 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp enum class LoadingSize { - SMALL, MEDIUM, LARGE + SMALL, MEDIUM, LARGE } @Composable fun LoadingIndicator( - modifier: Modifier = Modifier, - size: LoadingSize = LoadingSize.MEDIUM, - message: String? = null + modifier: Modifier = Modifier, + size: LoadingSize = LoadingSize.MEDIUM, + message: String? = null ) { - val indicatorSize = when (size) { - LoadingSize.SMALL -> 24.dp - LoadingSize.MEDIUM -> 32.dp - LoadingSize.LARGE -> 48.dp - } + val indicatorSize = when (size) { + LoadingSize.SMALL -> 24.dp + LoadingSize.MEDIUM -> 32.dp + LoadingSize.LARGE -> 48.dp + } - val strokeWidth = when (size) { - LoadingSize.SMALL -> 2.dp - LoadingSize.MEDIUM -> 3.dp - LoadingSize.LARGE -> 4.dp - } + val strokeWidth = when (size) { + LoadingSize.SMALL -> 2.dp + LoadingSize.MEDIUM -> 3.dp + LoadingSize.LARGE -> 4.dp + } - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - CircularProgressIndicator( - modifier = Modifier.size(indicatorSize), - strokeWidth = strokeWidth - ) + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(indicatorSize), + strokeWidth = strokeWidth + ) - if (message != null) { - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = message, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center - ) - } + if (message != null) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center + ) } + } } @Composable fun FullScreenLoading( - message: String = "Loading...", - modifier: Modifier = Modifier + message: String = "Loading...", + modifier: Modifier = Modifier ) { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - LoadingIndicator( - size = LoadingSize.LARGE, - message = message - ) - } + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + LoadingIndicator( + size = LoadingSize.LARGE, + message = message + ) + } } @Composable fun InlineLoading( - message: String? = null, - modifier: Modifier = Modifier + message: String? = null, + modifier: Modifier = Modifier ) { - Row( - modifier = modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - LoadingIndicator( - size = LoadingSize.SMALL, - message = message - ) - } + Row( + modifier = modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + LoadingIndicator( + size = LoadingSize.SMALL, + message = message + ) + } } @Composable fun LinearLoadingIndicator( - modifier: Modifier = Modifier, - message: String? = null + modifier: Modifier = Modifier, + message: String? = null ) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally - ) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth() - ) + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth() + ) - if (message != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = message, - style = MaterialTheme.typography.bodySmall, - textAlign = TextAlign.Center - ) - } + if (message != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = message, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center + ) } + } } diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleButton.kt b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleButton.kt index 477bb306..eb1b7019 100644 --- a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleButton.kt +++ b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleButton.kt @@ -1,125 +1,124 @@ package at.mocode.clients.shared.commonui.components import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp enum class ButtonVariant { - PRIMARY, SECONDARY, OUTLINE, TEXT + PRIMARY, SECONDARY, OUTLINE, TEXT } enum class ButtonSize { - SMALL, MEDIUM, LARGE + SMALL, MEDIUM, LARGE } @Composable fun MeldestelleButton( - text: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - variant: ButtonVariant = ButtonVariant.PRIMARY, - size: ButtonSize = ButtonSize.MEDIUM, - enabled: Boolean = true, - isLoading: Boolean = false, - fullWidth: Boolean = false + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + variant: ButtonVariant = ButtonVariant.PRIMARY, + size: ButtonSize = ButtonSize.MEDIUM, + enabled: Boolean = true, + isLoading: Boolean = false, + fullWidth: Boolean = false ) { - val buttonModifier = modifier.then( - if (fullWidth) Modifier.fillMaxWidth() else Modifier - ).then( - when (size) { - ButtonSize.SMALL -> Modifier.height(32.dp) - ButtonSize.MEDIUM -> Modifier.height(40.dp) - ButtonSize.LARGE -> Modifier.height(48.dp) - } - ) - - when (variant) { - ButtonVariant.PRIMARY -> Button( - onClick = onClick, - modifier = buttonModifier, - enabled = enabled && !isLoading - ) { - ButtonContent(text = text, isLoading = isLoading) - } - - ButtonVariant.SECONDARY -> FilledTonalButton( - onClick = onClick, - modifier = buttonModifier, - enabled = enabled && !isLoading - ) { - ButtonContent(text = text, isLoading = isLoading) - } - - ButtonVariant.OUTLINE -> OutlinedButton( - onClick = onClick, - modifier = buttonModifier, - enabled = enabled && !isLoading - ) { - ButtonContent(text = text, isLoading = isLoading) - } - - ButtonVariant.TEXT -> TextButton( - onClick = onClick, - modifier = buttonModifier, - enabled = enabled && !isLoading - ) { - ButtonContent(text = text, isLoading = isLoading) - } + val buttonModifier = modifier.then( + if (fullWidth) Modifier.fillMaxWidth() else Modifier + ).then( + when (size) { + ButtonSize.SMALL -> Modifier.height(32.dp) + ButtonSize.MEDIUM -> Modifier.height(40.dp) + ButtonSize.LARGE -> Modifier.height(48.dp) } + ) + + when (variant) { + ButtonVariant.PRIMARY -> Button( + onClick = onClick, + modifier = buttonModifier, + enabled = enabled && !isLoading + ) { + ButtonContent(text = text, isLoading = isLoading) + } + + ButtonVariant.SECONDARY -> FilledTonalButton( + onClick = onClick, + modifier = buttonModifier, + enabled = enabled && !isLoading + ) { + ButtonContent(text = text, isLoading = isLoading) + } + + ButtonVariant.OUTLINE -> OutlinedButton( + onClick = onClick, + modifier = buttonModifier, + enabled = enabled && !isLoading + ) { + ButtonContent(text = text, isLoading = isLoading) + } + + ButtonVariant.TEXT -> TextButton( + onClick = onClick, + modifier = buttonModifier, + enabled = enabled && !isLoading + ) { + ButtonContent(text = text, isLoading = isLoading) + } + } } @Composable private fun ButtonContent( - text: String, - isLoading: Boolean + text: String, + isLoading: Boolean ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.padding(2.dp), - strokeWidth = 2.dp - ) - } else { - Text(text) - } + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.padding(2.dp), + strokeWidth = 2.dp + ) + } else { + Text(text) + } } @Composable fun PrimaryButton( - text: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - isLoading: Boolean = false, - fullWidth: Boolean = false + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + isLoading: Boolean = false, + fullWidth: Boolean = false ) = MeldestelleButton( - text = text, - onClick = onClick, - modifier = modifier, - variant = ButtonVariant.PRIMARY, - enabled = enabled, - isLoading = isLoading, - fullWidth = fullWidth + text = text, + onClick = onClick, + modifier = modifier, + variant = ButtonVariant.PRIMARY, + enabled = enabled, + isLoading = isLoading, + fullWidth = fullWidth ) @Composable fun SecondaryButton( - text: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - isLoading: Boolean = false, - fullWidth: Boolean = false + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + isLoading: Boolean = false, + fullWidth: Boolean = false ) = MeldestelleButton( - text = text, - onClick = onClick, - modifier = modifier, - variant = ButtonVariant.SECONDARY, - enabled = enabled, - isLoading = isLoading, - fullWidth = fullWidth + text = text, + onClick = onClick, + modifier = modifier, + variant = ButtonVariant.SECONDARY, + enabled = enabled, + isLoading = isLoading, + fullWidth = fullWidth ) diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleTextField.kt b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleTextField.kt index 83160bca..5f678a4d 100644 --- a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleTextField.kt +++ b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleTextField.kt @@ -17,176 +17,177 @@ import androidx.compose.ui.unit.dp @Composable fun MeldestelleTextField( - value: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - label: String? = null, - placeholder: String? = null, - leadingIcon: ImageVector? = null, - trailingIcon: ImageVector? = null, - onTrailingIconClick: (() -> Unit)? = null, - isError: Boolean = false, - errorMessage: String? = null, - helperText: String? = null, - enabled: Boolean = true, - readOnly: Boolean = false, - singleLine: Boolean = true, - maxLines: Int = Int.MAX_VALUE, - keyboardType: KeyboardType = KeyboardType.Text, - imeAction: ImeAction = ImeAction.Default, - keyboardActions: KeyboardActions = KeyboardActions.Default, - visualTransformation: VisualTransformation = VisualTransformation.None + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String? = null, + placeholder: String? = null, + leadingIcon: ImageVector? = null, + trailingIcon: ImageVector? = null, + onTrailingIconClick: (() -> Unit)? = null, + isError: Boolean = false, + errorMessage: String? = null, + helperText: String? = null, + enabled: Boolean = true, + readOnly: Boolean = false, + singleLine: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + keyboardType: KeyboardType = KeyboardType.Text, + imeAction: ImeAction = ImeAction.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + visualTransformation: VisualTransformation = VisualTransformation.None ) { - Column(modifier = modifier) { - OutlinedTextField( - value = value, - onValueChange = onValueChange, - modifier = Modifier.fillMaxWidth(), - label = label?.let { { Text(it) } }, - placeholder = placeholder?.let { { Text(it) } }, - leadingIcon = leadingIcon?.let { icon -> - { Icon(imageVector = icon, contentDescription = null) } - }, - trailingIcon = if (trailingIcon != null) { - { - IconButton( - onClick = onTrailingIconClick ?: {} - ) { - Icon(imageVector = trailingIcon, contentDescription = null) - } - } - } else null, - isError = isError, - enabled = enabled, - readOnly = readOnly, - singleLine = singleLine, - maxLines = maxLines, - keyboardOptions = KeyboardOptions( - keyboardType = keyboardType, - imeAction = imeAction - ), - keyboardActions = keyboardActions, - visualTransformation = visualTransformation - ) - - // Error or helper text - when { - isError && errorMessage != null -> { - Text( - text = errorMessage, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(start = 16.dp, top = 4.dp) - ) - } - helperText != null -> { - Text( - text = helperText, - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(start = 16.dp, top = 4.dp) - ) - } + Column(modifier = modifier) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.fillMaxWidth(), + label = label?.let { { Text(it) } }, + placeholder = placeholder?.let { { Text(it) } }, + leadingIcon = leadingIcon?.let { icon -> + { Icon(imageVector = icon, contentDescription = null) } + }, + trailingIcon = if (trailingIcon != null) { + { + IconButton( + onClick = onTrailingIconClick ?: {} + ) { + Icon(imageVector = trailingIcon, contentDescription = null) + } } + } else null, + isError = isError, + enabled = enabled, + readOnly = readOnly, + singleLine = singleLine, + maxLines = maxLines, + keyboardOptions = KeyboardOptions( + keyboardType = keyboardType, + imeAction = imeAction + ), + keyboardActions = keyboardActions, + visualTransformation = visualTransformation + ) + + // Error or helper text + when { + isError && errorMessage != null -> { + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } + + helperText != null -> { + Text( + text = helperText, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } } + } } @Composable fun MeldestellePasswordField( - value: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - label: String = "Password", - placeholder: String? = null, - isError: Boolean = false, - errorMessage: String? = null, - helperText: String? = null, - enabled: Boolean = true, - imeAction: ImeAction = ImeAction.Done, - keyboardActions: KeyboardActions = KeyboardActions.Default + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String = "Password", + placeholder: String? = null, + isError: Boolean = false, + errorMessage: String? = null, + helperText: String? = null, + enabled: Boolean = true, + imeAction: ImeAction = ImeAction.Done, + keyboardActions: KeyboardActions = KeyboardActions.Default ) { - var passwordVisible by remember { mutableStateOf(false) } + var passwordVisible by remember { mutableStateOf(false) } - MeldestelleTextField( - value = value, - onValueChange = onValueChange, - modifier = modifier, - label = label, - placeholder = placeholder, - trailingIcon = if (passwordVisible) { - // You would need to import the actual icon from Material Icons - null // Placeholder for visibility off icon - } else { - null // Placeholder for visibility on icon - }, - onTrailingIconClick = { passwordVisible = !passwordVisible }, - isError = isError, - errorMessage = errorMessage, - helperText = helperText, - enabled = enabled, - keyboardType = KeyboardType.Password, - imeAction = imeAction, - keyboardActions = keyboardActions, - visualTransformation = if (passwordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - } - ) + MeldestelleTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + label = label, + placeholder = placeholder, + trailingIcon = if (passwordVisible) { + // You would need to import the actual icon from Material Icons + null // Placeholder for visibility off icon + } else { + null // Placeholder for visibility on icon + }, + onTrailingIconClick = { passwordVisible = !passwordVisible }, + isError = isError, + errorMessage = errorMessage, + helperText = helperText, + enabled = enabled, + keyboardType = KeyboardType.Password, + imeAction = imeAction, + keyboardActions = keyboardActions, + visualTransformation = if (passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + } + ) } @Composable fun MeldestelleEmailField( - value: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - label: String = "Email", - placeholder: String? = null, - isError: Boolean = false, - errorMessage: String? = null, - helperText: String? = null, - enabled: Boolean = true, - imeAction: ImeAction = ImeAction.Next, - keyboardActions: KeyboardActions = KeyboardActions.Default + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String = "Email", + placeholder: String? = null, + isError: Boolean = false, + errorMessage: String? = null, + helperText: String? = null, + enabled: Boolean = true, + imeAction: ImeAction = ImeAction.Next, + keyboardActions: KeyboardActions = KeyboardActions.Default ) { - MeldestelleTextField( - value = value, - onValueChange = onValueChange, - modifier = modifier, - label = label, - placeholder = placeholder, - isError = isError, - errorMessage = errorMessage, - helperText = helperText, - enabled = enabled, - keyboardType = KeyboardType.Email, - imeAction = imeAction, - keyboardActions = keyboardActions - ) + MeldestelleTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + label = label, + placeholder = placeholder, + isError = isError, + errorMessage = errorMessage, + helperText = helperText, + enabled = enabled, + keyboardType = KeyboardType.Email, + imeAction = imeAction, + keyboardActions = keyboardActions + ) } /** * Form validation utilities */ object FormValidation { - fun validateEmail(email: String): String? { - return when { - email.isEmpty() -> "Email is required" - !email.contains("@") -> "Invalid email format" - !email.matches(Regex("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$")) -> "Invalid email format" - else -> null - } + fun validateEmail(email: String): String? { + return when { + email.isEmpty() -> "Email is required" + !email.contains("@") -> "Invalid email format" + !email.matches(Regex("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$")) -> "Invalid email format" + else -> null } + } - fun validatePassword(password: String): String? { - return when { - password.isEmpty() -> "Password is required" - password.length < 6 -> "Password must be at least 6 characters" - else -> null - } + fun validatePassword(password: String): String? { + return when { + password.isEmpty() -> "Password is required" + password.length < 6 -> "Password must be at least 6 characters" + else -> null } + } - fun validateRequired(value: String, fieldName: String): String? { - return if (value.isEmpty()) "$fieldName is required" else null - } + fun validateRequired(value: String, fieldName: String): String? { + return if (value.isEmpty()) "$fieldName is required" else null + } } diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/NotificationCard.kt b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/NotificationCard.kt index e207e638..ecd16e32 100644 --- a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/NotificationCard.kt +++ b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/NotificationCard.kt @@ -1,179 +1,5 @@ package at.mocode.clients.shared.commonui.components -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import at.mocode.clients.shared.presentation.state.Notification -import at.mocode.clients.shared.presentation.state.NotificationType - -@Composable -fun NotificationCard( - notification: Notification, - onDismiss: () -> Unit, - modifier: Modifier = Modifier -) { - val backgroundColor = when (notification.type) { - NotificationType.SUCCESS -> Color(0xFF4CAF50).copy(alpha = 0.1f) - NotificationType.ERROR -> Color(0xFFF44336).copy(alpha = 0.1f) - NotificationType.WARNING -> Color(0xFFFF9800).copy(alpha = 0.1f) - NotificationType.INFO -> Color(0xFF2196F3).copy(alpha = 0.1f) - } - - val borderColor = when (notification.type) { - NotificationType.SUCCESS -> Color(0xFF4CAF50) - NotificationType.ERROR -> Color(0xFFF44336) - NotificationType.WARNING -> Color(0xFFFF9800) - NotificationType.INFO -> Color(0xFF2196F3) - } - - Card( - modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = backgroundColor), - shape = RoundedCornerShape(8.dp), - border = androidx.compose.foundation.BorderStroke(1.dp, borderColor) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.Top - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = notification.title, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold - ) - - if (notification.message.isNotBlank()) { - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = notification.message, - style = MaterialTheme.typography.bodySmall - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = notification.timestamp, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - IconButton( - onClick = onDismiss, - modifier = Modifier.size(24.dp) - ) { - Text("×", style = MaterialTheme.typography.titleMedium) - } - } - } -} - -@Composable -fun NotificationList( - notifications: List, - onDismissNotification: (String) -> Unit, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - notifications.forEach { notification -> - NotificationCard( - notification = notification, - onDismiss = { onDismissNotification(notification.id) } - ) - } - } -} - -@Composable -fun SnackbarNotification( - notification: Notification, - onDismiss: () -> Unit, - modifier: Modifier = Modifier -) { - val backgroundColor = when (notification.type) { - NotificationType.SUCCESS -> Color(0xFF4CAF50) - NotificationType.ERROR -> Color(0xFFF44336) - NotificationType.WARNING -> Color(0xFFFF9800) - NotificationType.INFO -> Color(0xFF2196F3) - } - - Snackbar( - modifier = modifier, - containerColor = backgroundColor, - contentColor = Color.White, - action = { - TextButton( - onClick = onDismiss, - colors = ButtonDefaults.textButtonColors( - contentColor = Color.White - ) - ) { - Text("Dismiss") - } - } - ) { - Column { - Text( - text = notification.title, - fontWeight = FontWeight.SemiBold - ) - if (notification.message.isNotBlank()) { - Text( - text = notification.message, - style = MaterialTheme.typography.bodySmall - ) - } - } - } -} - -@Composable -fun ToastNotification( - message: String, - type: NotificationType = NotificationType.INFO, - visible: Boolean, - onDismiss: () -> Unit, - modifier: Modifier = Modifier -) { - if (visible) { - LaunchedEffect(Unit) { - kotlinx.coroutines.delay(3000) // Auto dismiss after 3 seconds - onDismiss() - } - - val backgroundColor = when (type) { - NotificationType.SUCCESS -> Color(0xFF4CAF50) - NotificationType.ERROR -> Color(0xFFF44336) - NotificationType.WARNING -> Color(0xFFFF9800) - NotificationType.INFO -> Color(0xFF2196F3) - } - - Card( - modifier = modifier, - colors = CardDefaults.cardColors(containerColor = backgroundColor), - shape = RoundedCornerShape(24.dp) - ) { - Text( - text = message, - color = Color.White, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - style = MaterialTheme.typography.bodyMedium - ) - } - } -} +// Legacy notification components removed due to dependency on old presentation layer. +// Intentionally left empty as part of cleanup. You can safely delete this file +// if no modules import it anymore. diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/layout/MainLayout.kt b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/layout/MainLayout.kt deleted file mode 100644 index 9865faae..00000000 --- a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/layout/MainLayout.kt +++ /dev/null @@ -1,232 +0,0 @@ -package at.mocode.clients.shared.commonui.layout - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.material3.HorizontalDivider -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import at.mocode.clients.shared.commonui.components.* -import at.mocode.clients.shared.commonui.screens.LoginScreenContainer -import at.mocode.clients.shared.presentation.state.AppState -import at.mocode.clients.shared.presentation.actions.AppAction - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MainLayout( - appState: AppState, - onDispatchAction: (AppAction) -> Unit, - onNavigateTo: (String) -> Unit, - content: @Composable () -> Unit, - modifier: Modifier = Modifier -) { - var showUserMenu by remember { mutableStateOf(false) } - - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = { - Text( - text = "Meldestelle", - fontWeight = FontWeight.Bold - ) - }, - actions = { - // Notifications - if (appState.ui.notifications.isNotEmpty()) { - BadgedBox( - badge = { - Badge( - contentColor = MaterialTheme.colorScheme.onError, - containerColor = MaterialTheme.colorScheme.error - ) { - Text(appState.ui.notifications.size.toString()) - } - } - ) { - IconButton( - onClick = { onNavigateTo("/notifications") } - ) { - Text("🔔") - } - } - } else { - IconButton( - onClick = { onNavigateTo("/notifications") } - ) { - Text("🔔") - } - } - - // Theme toggle - IconButton( - onClick = { onDispatchAction(AppAction.UI.ToggleDarkMode) } - ) { - Text(if (appState.ui.isDarkMode) "☀️" else "🌙") - } - - // User menu - Box { - IconButton( - onClick = { showUserMenu = true } - ) { - Text("👤") - } - - DropdownMenu( - expanded = showUserMenu, - onDismissRequest = { showUserMenu = false } - ) { - DropdownMenuItem( - text = { - Column { - Text( - text = appState.auth.user?.firstName ?: "User", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold - ) - Text( - text = appState.auth.user?.email ?: "", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - }, - onClick = { - showUserMenu = false - onNavigateTo("/profile") - } - ) - - HorizontalDivider() - - DropdownMenuItem( - text = { Text("Settings") }, - onClick = { - showUserMenu = false - onNavigateTo("/settings") - } - ) - - DropdownMenuItem( - text = { Text("Help") }, - onClick = { - showUserMenu = false - onNavigateTo("/help") - } - ) - - HorizontalDivider() - - DropdownMenuItem( - text = { - Text( - text = "Logout", - color = MaterialTheme.colorScheme.error - ) - }, - onClick = { - showUserMenu = false - onDispatchAction(AppAction.Auth.Logout) - } - ) - } - } - } - ) - }, - bottomBar = { - if (appState.ui.notifications.isNotEmpty()) { - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceVariant - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = "${appState.ui.notifications.size} notification(s)", - style = MaterialTheme.typography.bodySmall - ) - TextButton( - onClick = { onNavigateTo("/notifications") } - ) { - Text("View All") - } - } - } - } - } - ) { paddingValues -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) { - // Loading overlay - if (appState.ui.isLoading) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FullScreenLoading("Loading...") - } - } else { - content() - } - } - } -} - -@Composable -fun AuthenticatedLayout( - appState: AppState, - onDispatchAction: (AppAction) -> Unit, - onNavigateTo: (String) -> Unit, - content: @Composable () -> Unit, - modifier: Modifier = Modifier -) { - if (appState.auth.isAuthenticated) { - MainLayout( - appState = appState, - onDispatchAction = onDispatchAction, - onNavigateTo = onNavigateTo, - content = content, - modifier = modifier - ) - } else { - // Show login screen if not authenticated - LoginScreenContainer( - authState = appState.auth, - onDispatchAction = onDispatchAction, - modifier = modifier - ) - } -} - -@Composable -fun ResponsiveLayout( - appState: AppState, - onDispatchAction: (AppAction) -> Unit, - onNavigateTo: (String) -> Unit, - content: @Composable (Boolean) -> Unit, - modifier: Modifier = Modifier -) { - // Simple responsive design - could be enhanced with actual screen size detection - val isCompact = remember { mutableStateOf(false) } - - AuthenticatedLayout( - appState = appState, - onDispatchAction = onDispatchAction, - onNavigateTo = onNavigateTo, - content = { content(isCompact.value) }, - modifier = modifier - ) -} diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/screens/DashboardScreen.kt b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/screens/DashboardScreen.kt deleted file mode 100644 index 902c180f..00000000 --- a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/screens/DashboardScreen.kt +++ /dev/null @@ -1,250 +0,0 @@ -package at.mocode.clients.shared.commonui.screens - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import at.mocode.clients.shared.commonui.components.* -import at.mocode.clients.shared.presentation.state.AppState -import at.mocode.clients.shared.presentation.actions.AppAction - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DashboardScreen( - appState: AppState, - onDispatchAction: (AppAction) -> Unit, - onNavigateTo: (String) -> Unit, - modifier: Modifier = Modifier -) { - val scrollState = rememberScrollState() - val user = appState.auth.user - - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // Welcome Header - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp) - ) { - Text( - text = "Welcome back, ${user?.firstName ?: "User"}!", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Here's what's happening in your Meldestelle dashboard", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) - ) - } - } - - // Quick Actions - Text( - text = "Quick Actions", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - PrimaryButton( - text = "New Report", - onClick = { onNavigateTo("/reports/new") }, - modifier = Modifier.weight(1f) - ) - SecondaryButton( - text = "View Reports", - onClick = { onNavigateTo("/reports") }, - modifier = Modifier.weight(1f) - ) - } - - // Statistics Cards - Text( - text = "Overview", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - StatisticCard( - title = "Total Reports", - value = "142", - modifier = Modifier.weight(1f) - ) - StatisticCard( - title = "Open Issues", - value = "23", - modifier = Modifier.weight(1f) - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - StatisticCard( - title = "Resolved", - value = "119", - modifier = Modifier.weight(1f) - ) - StatisticCard( - title = "This Month", - value = "18", - modifier = Modifier.weight(1f) - ) - } - - // Recent Activity - Text( - text = "Recent Activity", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold - ) - - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - ActivityItem( - title = "Report #1234 updated", - subtitle = "Status changed to 'In Progress'", - timestamp = "2 hours ago" - ) - HorizontalDivider() - ActivityItem( - title = "New report submitted", - subtitle = "Report #1235 - Urgent priority", - timestamp = "4 hours ago" - ) - HorizontalDivider() - ActivityItem( - title = "Report #1230 resolved", - subtitle = "Issue successfully closed", - timestamp = "1 day ago" - ) - } - } - - // Connection Status - if (!appState.network.isOnline) { - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "⚠️", - style = MaterialTheme.typography.titleLarge - ) - Spacer(modifier = Modifier.width(12.dp)) - Column { - Text( - text = "Offline Mode", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold - ) - Text( - text = "Some features may be limited", - style = MaterialTheme.typography.bodySmall - ) - } - } - } - } - } -} - -@Composable -private fun StatisticCard( - title: String, - value: String, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = value, - style = MaterialTheme.typography.headlineLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = title, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } -} - -@Composable -private fun ActivityItem( - title: String, - subtitle: String, - timestamp: String, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier - ) { - Text( - text = title, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Medium - ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = timestamp, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - } -} diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/screens/LoginScreen.kt b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/screens/LoginScreen.kt deleted file mode 100644 index 790f0347..00000000 --- a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/screens/LoginScreen.kt +++ /dev/null @@ -1,198 +0,0 @@ -package at.mocode.clients.shared.commonui.screens - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import at.mocode.clients.shared.commonui.components.* -import at.mocode.clients.shared.presentation.actions.AppAction -import at.mocode.clients.shared.presentation.state.AuthState - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LoginScreen( - authState: AuthState, - onLoginClick: (String, String) -> Unit, - onNavigateToRegister: () -> Unit = {}, - onForgotPassword: () -> Unit = {}, - modifier: Modifier = Modifier -) { - var username by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var usernameError by remember { mutableStateOf(null) } - var passwordError by remember { mutableStateOf(null) } - - val focusManager = LocalFocusManager.current - - // Validate form - val isFormValid = username.isNotBlank() && password.isNotBlank() && - usernameError == null && passwordError == null - - fun validateUsername(value: String) { - usernameError = FormValidation.validateRequired(value, "Username") - } - - fun validatePassword(value: String) { - passwordError = FormValidation.validatePassword(value) - } - - fun handleLogin() { - validateUsername(username) - validatePassword(password) - - if (isFormValid) { - onLoginClick(username.trim(), password) - } - } - - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // Header - Text( - text = "Meldestelle", - style = MaterialTheme.typography.headlineLarge, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center - ) - - Text( - text = "Sign in to your account", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // Username Field - MeldestelleTextField( - value = username, - onValueChange = { - username = it - if (usernameError != null) validateUsername(it) - }, - label = "Username", - placeholder = "Enter your username", - isError = usernameError != null, - errorMessage = usernameError, - enabled = !authState.isLoading, - imeAction = ImeAction.Next, - keyboardActions = KeyboardActions( - onNext = { focusManager.moveFocus(FocusDirection.Down) } - ) - ) - - // Password Field - MeldestellePasswordField( - value = password, - onValueChange = { - password = it - if (passwordError != null) validatePassword(it) - }, - label = "Password", - placeholder = "Enter your password", - isError = passwordError != null, - errorMessage = passwordError, - enabled = !authState.isLoading, - imeAction = ImeAction.Done, - keyboardActions = KeyboardActions( - onDone = { - focusManager.clearFocus() - if (isFormValid) handleLogin() - } - ) - ) - - // Error display - authState.error?.let { errorMessage -> - Text( - text = errorMessage, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - textAlign = TextAlign.Center - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - // Login Button - PrimaryButton( - text = "Sign In", - onClick = ::handleLogin, - enabled = isFormValid && !authState.isLoading, - isLoading = authState.isLoading, - fullWidth = true - ) - - // Forgot Password - TextButton( - onClick = onForgotPassword, - enabled = !authState.isLoading - ) { - Text("Forgot Password?") - } - - HorizontalDivider() - - // Register Link - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Don't have an account?", - style = MaterialTheme.typography.bodyMedium - ) - Spacer(modifier = Modifier.width(4.dp)) - TextButton( - onClick = onNavigateToRegister, - enabled = !authState.isLoading - ) { - Text("Sign Up") - } - } - } - } - } -} - -@Composable -fun LoginScreenContainer( - authState: AuthState, - onDispatchAction: (AppAction) -> Unit, - onNavigateToRegister: () -> Unit = {}, - onForgotPassword: () -> Unit = {}, - modifier: Modifier = Modifier -) { - LoginScreen( - authState = authState, - onLoginClick = { username, password -> - onDispatchAction(AppAction.Auth.LoginStart(username, password)) - }, - onNavigateToRegister = onNavigateToRegister, - onForgotPassword = onForgotPassword, - modifier = modifier - ) -} diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/theme/AppTheme.kt b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/theme/AppTheme.kt index 08fecf66..a22248c9 100644 --- a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/theme/AppTheme.kt +++ b/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/theme/AppTheme.kt @@ -8,42 +8,42 @@ import androidx.compose.ui.graphics.Color // Define custom colors for the app private val LightColorScheme = lightColorScheme( - primary = Color(0xFF1976D2), - onPrimary = Color.White, - primaryContainer = Color(0xFFBBDEFB), - onPrimaryContainer = Color(0xFF0D47A1), - secondary = Color(0xFF03DAC6), - onSecondary = Color.Black, - tertiary = Color(0xFF03A9F4), - background = Color(0xFFFAFAFA), - surface = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F) + primary = Color(0xFF1976D2), + onPrimary = Color.White, + primaryContainer = Color(0xFFBBDEFB), + onPrimaryContainer = Color(0xFF0D47A1), + secondary = Color(0xFF03DAC6), + onSecondary = Color.Black, + tertiary = Color(0xFF03A9F4), + background = Color(0xFFFAFAFA), + surface = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F) ) private val DarkColorScheme = darkColorScheme( - primary = Color(0xFF90CAF9), - onPrimary = Color(0xFF0D47A1), - primaryContainer = Color(0xFF1565C0), - onPrimaryContainer = Color(0xFFBBDEFB), - secondary = Color(0xFF03DAC6), - onSecondary = Color.Black, - tertiary = Color(0xFF03A9F4), - background = Color(0xFF121212), - surface = Color(0xFF1E1E1E), - onBackground = Color(0xFFE0E0E0), - onSurface = Color(0xFFE0E0E0) + primary = Color(0xFF90CAF9), + onPrimary = Color(0xFF0D47A1), + primaryContainer = Color(0xFF1565C0), + onPrimaryContainer = Color(0xFFBBDEFB), + secondary = Color(0xFF03DAC6), + onSecondary = Color.Black, + tertiary = Color(0xFF03A9F4), + background = Color(0xFF121212), + surface = Color(0xFF1E1E1E), + onBackground = Color(0xFFE0E0E0), + onSurface = Color(0xFFE0E0E0) ) @Composable fun AppTheme( - darkTheme: Boolean = false, // For now, we'll default to light theme - content: @Composable () -> Unit + darkTheme: Boolean = false, // For now, we'll default to light theme + content: @Composable () -> Unit ) { - val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme + val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme - MaterialTheme( - colorScheme = colorScheme, - content = content - ) + MaterialTheme( + colorScheme = colorScheme, + content = content + ) } diff --git a/clients/shared/navigation/build.gradle.kts b/clients/shared/navigation/build.gradle.kts index 06442479..b54b1df3 100644 --- a/clients/shared/navigation/build.gradle.kts +++ b/clients/shared/navigation/build.gradle.kts @@ -3,36 +3,38 @@ * Es ist noch simpler. */ plugins { - alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinMultiplatform) } group = "at.mocode.clients.shared" version = "1.0.0" kotlin { - val enableWasm = providers.gradleProperty("enableWasm").orNull == "true" + val enableWasm = providers.gradleProperty("enableWasm").orNull == "true" - jvmToolchain(21) + jvmToolchain(21) - jvm() + jvm() - js { - browser() + js { + browser() + binaries.executable() + } + + if (enableWasm) { + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() } + } - if (enableWasm) { - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { - browser() - } + sourceSets { + commonMain.dependencies { + // No specific dependencies needed for navigation routes } - - sourceSets { - commonMain.dependencies { - // No specific dependencies needed for navigation routes - } - commonTest.dependencies { - implementation(libs.kotlin.test) - } + commonTest.dependencies { + implementation(libs.kotlin.test) } + } } diff --git a/clients/shared/navigation/src/commonMain/kotlin/at/mocode/clients/shared/navigation/AppScreen.kt b/clients/shared/navigation/src/commonMain/kotlin/at/mocode/clients/shared/navigation/AppScreen.kt index bf33a797..142e7ec1 100644 --- a/clients/shared/navigation/src/commonMain/kotlin/at/mocode/clients/shared/navigation/AppScreen.kt +++ b/clients/shared/navigation/src/commonMain/kotlin/at/mocode/clients/shared/navigation/AppScreen.kt @@ -1,8 +1,9 @@ package at.mocode.clients.shared.navigation sealed class AppScreen { - data object Home : AppScreen() - data object Login : AppScreen() - data object Ping : AppScreen() - data object Profile : AppScreen() + data object Home : AppScreen() + data object Login : AppScreen() + data object Ping : AppScreen() + data object Profile : AppScreen() + data object AuthCallback : AppScreen() } diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/AppConfig.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/AppConfig.kt deleted file mode 100644 index 1660e9b7..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/AppConfig.kt +++ /dev/null @@ -1,17 +0,0 @@ -package at.mocode.clients.shared - -/** - * Zentrale App-Konfiguration für alle Client-Module. - * Hinweis: Diese Werte sind zentrale Defaults für DEV. Für PROD sollten sie - * via Build-Injektion (Gradle/ENV) überschrieben werden. Ein einfaches - * BuildConfig-Setup kann später ergänzt werden. - */ -object AppConfig { - // Gateway Basis-URL (API Gateway) - const val GATEWAY_URL: String = "http://localhost:8081" - - // Keycloak Konfiguration - const val KEYCLOAK_URL: String = "http://localhost:8180" - const val KEYCLOAK_REALM: String = "meldestelle" - const val KEYCLOAK_CLIENT_ID: String = "meldestelle-frontend" -} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/core/AppConfig.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/core/AppConfig.kt new file mode 100644 index 00000000..e8bb115c --- /dev/null +++ b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/core/AppConfig.kt @@ -0,0 +1,12 @@ +package at.mocode.clients.shared.core + +data class AppConfig( + val gatewayUrl: String, + val isDebug: Boolean +) + +// Standard-Config für Local Development +val devConfig = AppConfig( + gatewayUrl = "http://localhost:8081", + isDebug = true +) diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/core/AppConstants.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/core/AppConstants.kt new file mode 100644 index 00000000..76f31daf --- /dev/null +++ b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/core/AppConstants.kt @@ -0,0 +1,47 @@ +package at.mocode.clients.shared.core + +/** + * Shared application configuration constants for clients. + * These defaults target local development environments. + */ +object AppConstants { + // Gateway base URL (reverse proxy / API gateway) + const val GATEWAY_URL: String = "http://localhost:8081" + + // Keycloak configuration + const val KEYCLOAK_URL: String = "http://localhost:8180" + const val KEYCLOAK_REALM: String = "meldestelle" + + // Use public client configured in realm import: `web-app` + const val KEYCLOAK_CLIENT_ID: String = "web-app" + + // Default redirect URI for web PKCE flow (served by Nginx in web image) + // We use the root path so Keycloak can redirect back to /?code=... + fun webRedirectUri(): String = "http://localhost:4000/" + + fun registerUrl(): String = + "$KEYCLOAK_URL/realms/$KEYCLOAK_REALM/protocol/openid-connect/registrations?client_id=$KEYCLOAK_CLIENT_ID&response_type=code&redirect_uri=${ + encode( + webRedirectUri() + ) + }" + + fun loginUrl(): String = + "$KEYCLOAK_URL/realms/$KEYCLOAK_REALM/protocol/openid-connect/auth?client_id=$KEYCLOAK_CLIENT_ID&response_type=code&redirect_uri=${ + encode( + webRedirectUri() + ) + }" + + fun authorizeEndpoint(): String = + "$KEYCLOAK_URL/realms/$KEYCLOAK_REALM/protocol/openid-connect/auth" + + fun tokenEndpoint(): String = + "$KEYCLOAK_URL/realms/$KEYCLOAK_REALM/protocol/openid-connect/token" + + fun desktopDownloadUrl(): String = "http://localhost:4000/downloads/" + + // Helper to URL-encode values (very small percent-encoding sufficient for URIs here) + private fun encode(value: String): String = + value.replace("://", ":%2F%2F").replace("/", "%2F").replace(":", "%3A") +} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/data/repository/AuthRepository.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/data/repository/AuthRepository.kt deleted file mode 100644 index e27de814..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/data/repository/AuthRepository.kt +++ /dev/null @@ -1,171 +0,0 @@ -package at.mocode.clients.shared.data.repository - -import at.mocode.clients.shared.domain.models.User -import at.mocode.clients.shared.domain.models.AuthToken -import at.mocode.clients.shared.domain.models.ApiResponse -import at.mocode.clients.shared.network.HttpClientConfig -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.client.request.forms.* -import io.ktor.http.* -import kotlinx.serialization.Serializable - -/** - * Authentication repository handling all authentication-related operations - * with Keycloak integration. - */ -class AuthRepository( - private val baseUrl: String = "http://localhost:8080", - private val keycloakUrl: String = "http://localhost:8180", - private val realm: String = "meldestelle", - private val clientId: String = "meldestelle-client" -) : Repository { - - private val httpClient: HttpClient = HttpClientConfig.createClient(baseUrl) - - @Serializable - data class LoginRequest( - val username: String, - val password: String - ) - - @Serializable - data class KeycloakTokenResponse( - val access_token: String, - val refresh_token: String, - val expires_in: Long, - val token_type: String = "Bearer" - ) - - /** - * Authenticate user with username and password via Keycloak - */ - suspend fun login(username: String, password: String): RepositoryResult { - return try { - val response = httpClient.submitForm( - url = "$keycloakUrl/realms/$realm/protocol/openid-connect/token", - formParameters = Parameters.build { - append("grant_type", "password") - append("client_id", clientId) - append("username", username) - append("password", password) - } - ).body() - - val authToken = AuthToken( - accessToken = response.access_token, - refreshToken = response.refresh_token, - expiresIn = response.expires_in, - tokenType = response.token_type - ) - - RepositoryResult.Success(authToken) - } catch (e: Exception) { - RepositoryResult.Error( - at.mocode.clients.shared.domain.models.ApiError( - code = "LOGIN_FAILED", - message = "Login failed: ${e.message}" - ) - ) - } - } - - /** - * Refresh authentication token - */ - suspend fun refreshToken(refreshToken: String): RepositoryResult { - return try { - val response = httpClient.submitForm( - url = "$keycloakUrl/realms/$realm/protocol/openid-connect/token", - formParameters = Parameters.build { - append("grant_type", "refresh_token") - append("client_id", clientId) - append("refresh_token", refreshToken) - } - ).body() - - val authToken = AuthToken( - accessToken = response.access_token, - refreshToken = response.refresh_token, - expiresIn = response.expires_in, - tokenType = response.token_type - ) - - RepositoryResult.Success(authToken) - } catch (e: Exception) { - RepositoryResult.Error( - at.mocode.clients.shared.domain.models.ApiError( - code = "TOKEN_REFRESH_FAILED", - message = "Token refresh failed: ${e.message}" - ) - ) - } - } - - /** - * Get current user information using access token - */ - suspend fun getCurrentUser(accessToken: String): RepositoryResult { - return try { - val response = httpClient.get("$baseUrl/api/auth/me") { - header("Authorization", "Bearer $accessToken") - }.body>() - - response.toRepositoryResult() - } catch (e: Exception) { - RepositoryResult.Error( - at.mocode.clients.shared.domain.models.ApiError( - code = "USER_INFO_FAILED", - message = "Failed to get user info: ${e.message}" - ) - ) - } - } - - /** - * Logout user by invalidating tokens - */ - suspend fun logout(refreshToken: String): RepositoryResult { - return try { - httpClient.submitForm( - url = "$keycloakUrl/realms/$realm/protocol/openid-connect/logout", - formParameters = Parameters.build { - append("client_id", clientId) - append("refresh_token", refreshToken) - } - ) - - RepositoryResult.Success(Unit) - } catch (e: Exception) { - RepositoryResult.Error( - at.mocode.clients.shared.domain.models.ApiError( - code = "LOGOUT_FAILED", - message = "Logout failed: ${e.message}" - ) - ) - } - } - - /** - * Check if token is still valid - */ - suspend fun validateToken(accessToken: String): RepositoryResult { - return try { - val response = httpClient.get("$baseUrl/api/auth/validate") { - header("Authorization", "Bearer $accessToken") - }.body>() - - response.toRepositoryResult() - } catch (e: Exception) { - RepositoryResult.Success(false) // Token is invalid - } - } - - /** - * Cleanup resources - */ - fun close() { - httpClient.close() - } -} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/data/repository/PingRepositoryImpl.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/data/repository/PingRepositoryImpl.kt new file mode 100644 index 00000000..ddcf01a1 --- /dev/null +++ b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/data/repository/PingRepositoryImpl.kt @@ -0,0 +1,27 @@ +package at.mocode.clients.shared.data.repository + +import at.mocode.clients.shared.domain.model.PingData +import at.mocode.clients.shared.domain.model.Resource +import at.mocode.clients.shared.domain.repository.PingRepository +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* + +class PingRepositoryImpl( + private val httpClient: HttpClient +) : PingRepository { + + override suspend fun checkSystemStatus(): Resource { + return try { + // Der HttpClient hat die BaseURL schon konfiguriert (siehe NetworkModule) + val response = httpClient.get("/api/ping/simple").body() + Resource.Success(response) + } catch (e: Exception) { + // Hier fangen wir Netzwerkfehler ab und machen sie "hübsch" für die UI + Resource.Error( + message = "Verbindung fehlgeschlagen: ${e.message ?: "Unbekannter Fehler"}", + code = "NETWORK_ERROR" + ) + } + } +} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/data/repository/Repository.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/data/repository/Repository.kt deleted file mode 100644 index dc401775..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/data/repository/Repository.kt +++ /dev/null @@ -1,73 +0,0 @@ -package at.mocode.clients.shared.data.repository - -import at.mocode.clients.shared.domain.models.ApiResponse -import at.mocode.clients.shared.domain.models.ApiError - -/** - * Base repository interface defining common operations and patterns - * for data access across the application. - */ -interface Repository - -/** - * Result wrapper for repository operations to handle success/error states - */ -sealed class RepositoryResult { - data class Success(val data: T) : RepositoryResult() - data class Error(val error: ApiError) : RepositoryResult() - data class Loading(val message: String = "Loading...") : RepositoryResult() - - fun isSuccess(): Boolean = this is Success - fun isError(): Boolean = this is Error - fun isLoading(): Boolean = this is Loading - - fun getOrNull(): T? = when (this) { - is Success -> data - else -> null - } - - fun getErrorOrNull(): ApiError? = when (this) { - is Error -> error - else -> null - } -} - -/** - * Extension function to convert ApiResponse to RepositoryResult - */ -fun ApiResponse.toRepositoryResult(): RepositoryResult { - return if (success && data != null) { - RepositoryResult.Success(data) - } else { - RepositoryResult.Error( - error ?: ApiError( - code = "UNKNOWN_ERROR", - message = "Unknown error occurred" - ) - ) - } -} - -/** - * Extension function to handle repository results with callbacks - */ -inline fun RepositoryResult.onSuccess(action: (T) -> Unit): RepositoryResult { - if (this is RepositoryResult.Success) { - action(data) - } - return this -} - -inline fun RepositoryResult.onError(action: (ApiError) -> Unit): RepositoryResult { - if (this is RepositoryResult.Error) { - action(error) - } - return this -} - -inline fun RepositoryResult.onLoading(action: (String) -> Unit): RepositoryResult { - if (this is RepositoryResult.Loading) { - action(message) - } - return this -} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/di/NetworkModule.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/di/NetworkModule.kt new file mode 100644 index 00000000..dd1bbfdc --- /dev/null +++ b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/di/NetworkModule.kt @@ -0,0 +1,50 @@ +package at.mocode.clients.shared.di + +import at.mocode.clients.shared.core.AppConfig +import io.ktor.client.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json +import org.koin.dsl.module + +val networkModule = module { + // 1. JSON Konfiguration (Global verfügbar) + single { + Json { + ignoreUnknownKeys = true + prettyPrint = true + isLenient = true + } + } + + // 2. HttpClient (Singleton) + single { + val config = get() + val jsonConfig = get() + + HttpClient { + // Standard-URL setzen + defaultRequest { + url(config.gatewayUrl) + contentType(ContentType.Application.Json) + } + + install(ContentNegotiation) { + json(jsonConfig) + } + + install(Logging) { + level = if (config.isDebug) LogLevel.INFO else LogLevel.NONE + logger = Logger.DEFAULT + } + + install(HttpTimeout) { + requestTimeoutMillis = 10000 + connectTimeoutMillis = 10000 + } + } + } +} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/di/SharedModule.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/di/SharedModule.kt new file mode 100644 index 00000000..f548bf43 --- /dev/null +++ b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/di/SharedModule.kt @@ -0,0 +1,23 @@ +package at.mocode.clients.shared.di + +import at.mocode.clients.shared.core.devConfig +import org.koin.core.context.startKoin +import org.koin.dsl.KoinAppDeclaration +import org.koin.dsl.module + +// Das Modul für die Config +val configModule = module { + single { devConfig } // Später können wir hier PROD/DEV umschalten +} + +// Alle Module zusammen +val sharedModules = listOf( + configModule, + networkModule +) + +// Helper zum Starten von Koin (wird von der App aufgerufen) +fun initKoin(appDeclaration: KoinAppDeclaration = {}) = startKoin { + appDeclaration() + modules(sharedModules) +} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/model/ApiModels.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/model/ApiModels.kt new file mode 100644 index 00000000..4eeb71af --- /dev/null +++ b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/model/ApiModels.kt @@ -0,0 +1,39 @@ +package at.mocode.clients.shared.domain.model + +import kotlinx.serialization.Serializable + +/** + * Generischer Wrapper für API-Antworten. + */ +@Serializable +data class ApiResponse( + val success: Boolean, + val data: T? = null, + val error: ApiError? = null +) + +@Serializable +data class ApiError( + val code: String, + val message: String +) + +/** + * Das Ergebnis eines Repository-Aufrufs. + * Die UI kennt nur das hier, keine HTTP-Exceptions! + */ +sealed class Resource { + data class Success(val data: T) : Resource() + data class Error(val message: String, val code: String? = null) : Resource() + data object Loading : Resource() +} + +/** + * Datenmodell für den Ping. + */ +@Serializable +data class PingData( + val status: String, + val timestamp: String, + val service: String +) diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/models/ApiResponse.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/models/ApiResponse.kt deleted file mode 100644 index 3654282b..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/models/ApiResponse.kt +++ /dev/null @@ -1,27 +0,0 @@ -package at.mocode.clients.shared.domain.models - -import kotlinx.serialization.Serializable - -@Serializable -data class ApiResponse( - val success: Boolean, - val data: T? = null, - val error: ApiError? = null, - val timestamp: String, - val correlationId: String? = null -) - -@Serializable -data class ApiError( - val code: String, - val message: String, - val details: Map = emptyMap() -) - -@Serializable -data class HealthResponse( - val status: String, - val timestamp: String, - val service: String, - val healthy: Boolean -) diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/models/User.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/models/User.kt deleted file mode 100644 index 6bcfe493..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/models/User.kt +++ /dev/null @@ -1,22 +0,0 @@ -package at.mocode.clients.shared.domain.models - -import kotlinx.serialization.Serializable - -@Serializable -data class User( - val id: String, - val username: String, - val email: String, - val firstName: String, - val lastName: String, - val roles: Set = emptySet(), - val isActive: Boolean = true -) - -@Serializable -data class AuthToken( - val accessToken: String, - val refreshToken: String, - val expiresIn: Long, - val tokenType: String = "Bearer" -) diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/repository/PingRepository.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/repository/PingRepository.kt new file mode 100644 index 00000000..89672780 --- /dev/null +++ b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/domain/repository/PingRepository.kt @@ -0,0 +1,8 @@ +package at.mocode.clients.shared.domain.repository + +import at.mocode.clients.shared.domain.model.PingData +import at.mocode.clients.shared.domain.model.Resource + +interface PingRepository { + suspend fun checkSystemStatus(): Resource +} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/navigation/DeepLinkHandler.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/navigation/DeepLinkHandler.kt deleted file mode 100644 index 642e2bb2..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/navigation/DeepLinkHandler.kt +++ /dev/null @@ -1,194 +0,0 @@ -package at.mocode.clients.shared.navigation - -import at.mocode.clients.shared.presentation.store.AppStore - -/** - * Deep link handling for the application - */ -class DeepLinkHandler( - private val navigationManager: NavigationManager, - private val store: AppStore -) { - - /** - * Deep link configuration - */ - data class DeepLinkConfig( - val scheme: String = "meldestelle", - val host: String = "app", - val allowedDomains: Set = setOf("meldestelle.com", "localhost") - ) - - private val config = DeepLinkConfig() - - /** - * Handle a deep link URL - */ - fun handleDeepLink(url: String): Boolean { - return try { - val parsedLink = parseDeepLink(url) - if (parsedLink != null) { - processDeepLink(parsedLink) - true - } else { - false - } - } catch (e: Exception) { - // Log error in real implementation - false - } - } - - /** - * Parse deep link URL into components - */ - private fun parseDeepLink(url: String): DeepLink? { - return when { - url.startsWith("${config.scheme}://") -> parseCustomSchemeLink(url) - url.startsWith("https://") || url.startsWith("http://") -> parseWebLink(url) - else -> null - } - } - - /** - * Parse custom scheme deep links (e.g., meldestelle://app/dashboard) - */ - private fun parseCustomSchemeLink(url: String): DeepLink? { - val withoutScheme = url.removePrefix("${config.scheme}://") - val parts = withoutScheme.split("/") - - if (parts.isEmpty() || parts[0] != config.host) { - return null - } - - val path = "/" + parts.drop(1).joinToString("/") - val route = if (path == "/") Routes.HOME else path - - return DeepLink( - type = DeepLinkType.CUSTOM_SCHEME, - route = route, - params = RouteUtils.parseRouteParams(route), - originalUrl = url - ) - } - - /** - * Parse web deep links (e.g., https://meldestelle.com/dashboard) - */ - private fun parseWebLink(url: String): DeepLink? { - // Simple URL parsing - in real implementation use proper URL parser - val urlParts = url.split("/") - if (urlParts.size < 3) return null - - val domain = urlParts[2] - if (!config.allowedDomains.contains(domain)) { - return null - } - - val path = "/" + urlParts.drop(3).joinToString("/") - val route = if (path == "/" || path.isEmpty()) Routes.HOME else path - - return DeepLink( - type = DeepLinkType.WEB_LINK, - route = route, - params = RouteUtils.parseRouteParams(route), - originalUrl = url - ) - } - - /** - * Process a parsed deep link - */ - private fun processDeepLink(deepLink: DeepLink) { - val authState = store.state.value.auth - val cleanRoute = RouteUtils.getCleanRoute(deepLink.route) - - // Check if route requires authentication - if (RouteUtils.requiresAuth(cleanRoute)) { - if (!authState.isAuthenticated) { - // Save the intended route and redirect to log in - saveIntendedRoute(deepLink.route) - navigationManager.navigateTo(Routes.Auth.LOGIN) - return - } - } - - // Check if route requires admin privileges - if (RouteUtils.requiresAdmin(cleanRoute)) { - val hasAdminRole = authState.user?.roles?.contains("admin") ?: false - if (!hasAdminRole) { - // Redirect to unauthorized or home - navigationManager.navigateTo(Routes.HOME) - return - } - } - - // Navigate to the route - navigationManager.navigateTo(deepLink.route) - } - - /** - * Save the intended route for after authentication - */ - private fun saveIntendedRoute(route: String) { - // In real implementation, save to persistent storage - // For now; we'll store it in a simple variable - intendedRoute = route - } - - /** - * Get and clear the intended route - */ - fun getAndClearIntendedRoute(): String? { - val route = intendedRoute - intendedRoute = null - return route - } - - /** - * Check if there's a pending intended route - */ - fun hasIntendedRoute(): Boolean = intendedRoute != null - - /** - * Generate a deep link for a route - */ - fun generateDeepLink(route: String, useCustomScheme: Boolean = true): String { - return if (useCustomScheme) { - "${config.scheme}://${config.host}$route" - } else { - "https://${config.allowedDomains.first()}$route" - } - } - - /** - * Validate if a route is valid for deep linking - */ - fun isValidDeepLinkRoute(route: String): Boolean { - return RouteUtils.isValidRoute(route) && - !route.startsWith("/auth/") && // Auth routes shouldn't be deep linked - route != Routes.Auth.LOGIN - } - - companion object { - private var intendedRoute: String? = null - } -} - -/** - * Deep link data class - */ -data class DeepLink( - val type: DeepLinkType, - val route: String, - val params: Map, - val originalUrl: String -) - -/** - * Types of deep links - */ -enum class DeepLinkType { - CUSTOM_SCHEME, // meldestelle://app/route - WEB_LINK // https://meldestelle.com/route -} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/navigation/NavigationManager.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/navigation/NavigationManager.kt deleted file mode 100644 index 1faea4ea..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/navigation/NavigationManager.kt +++ /dev/null @@ -1,179 +0,0 @@ -package at.mocode.clients.shared.navigation - -import at.mocode.clients.shared.presentation.actions.AppAction -import at.mocode.clients.shared.presentation.store.AppStore -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -/** - * Navigation manager for handling routing and navigation logic - */ -class NavigationManager( - private val store: AppStore -) { - - /** - * Current route as a flow - */ - val currentRoute: Flow = store.state.map { it.navigation.currentRoute } - - /** - * Navigation history as a flow - */ - val navigationHistory: Flow> = store.state.map { it.navigation.history } - - /** - * Can go back flag as a flow - */ - val canGoBack: Flow = store.state.map { it.navigation.canGoBack } - - /** - * Navigate to a specific route - */ - fun navigateTo(route: String) { - store.dispatch(AppAction.Navigation.NavigateTo(route)) - } - - /** - * Navigate back to the previous route - */ - fun navigateBack() { - store.dispatch(AppAction.Navigation.NavigateBack) - } - - /** - * Replace current route without adding to history - */ - fun replaceRoute(route: String) { - store.dispatch(AppAction.Navigation.UpdateHistory(route)) - } - - /** - * Clear navigation history and navigate to the route - */ - fun navigateAndClearHistory(route: String) { - // First clear by replacing with the new route - store.dispatch(AppAction.Navigation.UpdateHistory(route)) - } - - /** - * Get current route value (non-reactive) - */ - fun getCurrentRoute(): String = store.state.value.navigation.currentRoute - - /** - * Check if we can navigate back - */ - fun canNavigateBack(): Boolean = store.state.value.navigation.canGoBack -} - -/** - * Route definitions for the application - */ -object Routes { - const val HOME = "/" - const val LOGIN = "/login" - const val DASHBOARD = "/dashboard" - const val PROFILE = "/profile" - const val SETTINGS = "/settings" - const val PING = "/ping" - - // Auth-related routes - object Auth { - const val LOGIN = "/auth/login" - const val LOGOUT = "/auth/logout" - const val REGISTER = "/auth/register" - const val FORGOT_PASSWORD = "/auth/forgot-password" - } - - // Admin routes - object Admin { - const val DASHBOARD = "/admin/dashboard" - const val USERS = "/admin/users" - const val SETTINGS = "/admin/settings" - } - - // Feature routes - object Features { - const val PING = "/features/ping" - const val REPORTS = "/features/reports" - const val NOTIFICATIONS = "/features/notifications" - } -} - -/** - * Route validation and utilities - */ -object RouteUtils { - - /** - * Check if a route requires authentication - */ - fun requiresAuth(route: String): Boolean { - return when { - route.startsWith("/auth/") && route != Routes.Auth.LOGIN -> false - route == Routes.HOME -> false - route == Routes.LOGIN -> false - else -> true - } - } - - /** - * Check if a route is for admin only - */ - fun requiresAdmin(route: String): Boolean { - return route.startsWith("/admin/") - } - - /** - * Get the default route for authenticated users - */ - fun getDefaultAuthenticatedRoute(): String = Routes.DASHBOARD - - /** - * Get the default route for unauthenticated users - */ - fun getDefaultUnauthenticatedRoute(): String = Routes.LOGIN - - /** - * Validate route format - */ - fun isValidRoute(route: String): Boolean { - return route.startsWith("/") && route.isNotBlank() - } - - /** - * Parse route parameters (simple implementation) - */ - fun parseRouteParams(route: String): Map { - val params = mutableMapOf() - - // Simple query parameter parsing - if (route.contains("?")) { - val parts = route.split("?") - if (parts.size == 2) { - val queryParams = parts[1].split("&") - queryParams.forEach { param -> - val keyValue = param.split("=") - if (keyValue.size == 2) { - params[keyValue[0]] = keyValue[1] - } - } - } - } - - return params - } - - /** - * Get clean route without parameters - */ - fun getCleanRoute(route: String): String { - return if (route.contains("?")) { - route.split("?")[0] - } else { - route - } - } -} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/navigation/NavigationPersistence.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/navigation/NavigationPersistence.kt deleted file mode 100644 index 0ba9926c..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/navigation/NavigationPersistence.kt +++ /dev/null @@ -1,74 +0,0 @@ -package at.mocode.clients.shared.navigation - -import at.mocode.clients.shared.presentation.state.NavigationState -import kotlinx.coroutines.flow.Flow - -/** - * Interface für das Persistieren von Navigation State - */ -interface NavigationPersistence { - suspend fun saveNavigationState(state: NavigationState) - fun getNavigationState(): Flow - suspend fun clearNavigationState() -} - -/** - * Default implementation ohne echte Persistierung (In-Memory) - * Platform-spezifische Implementierungen können echte Persistierung bereitstellen - */ -class DefaultNavigationPersistence : NavigationPersistence { - private var currentState: NavigationState? = null - - override suspend fun saveNavigationState(state: NavigationState) { - currentState = state - } - - override fun getNavigationState(): Flow { - return kotlinx.coroutines.flow.flowOf(currentState) - } - - override suspend fun clearNavigationState() { - currentState = null - } -} - -/** - * Navigation History Manager mit Persistierung - */ -class NavigationHistoryManager( - private val persistence: NavigationPersistence -) { - companion object { - private const val MAX_HISTORY_SIZE = 50 - } - - suspend fun saveRoute(route: String, history: List) { - val state = NavigationState( - currentRoute = route, - history = history.takeLast(MAX_HISTORY_SIZE), - canGoBack = history.isNotEmpty() - ) - persistence.saveNavigationState(state) - } - - fun getPersistedState() = persistence.getNavigationState() - - suspend fun clear() = persistence.clearNavigationState() - - /** - * Optimiert die History für bessere Performance - */ - private fun optimizeHistory(history: List): List { - // Entfernt Duplikate in Folge und behält nur die letzten N Einträge - return history - .fold(emptyList()) { acc, route -> - if (acc.lastOrNull() != route) acc + route else acc - } - .takeLast(MAX_HISTORY_SIZE) - } - - suspend fun addToHistory(newRoute: String, currentHistory: List) { - val optimizedHistory = optimizeHistory(currentHistory + newRoute) - saveRoute(newRoute, optimizedHistory.dropLast(1)) - } -} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/network/HttpClientConfig.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/network/HttpClientConfig.kt deleted file mode 100644 index 9a0c9c68..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/network/HttpClientConfig.kt +++ /dev/null @@ -1,27 +0,0 @@ -package at.mocode.clients.shared.network - -import io.ktor.client.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.serialization.kotlinx.json.* -import kotlinx.serialization.json.Json - -object HttpClientConfig { - - fun createClient( - baseUrl: String = "http://localhost:8080" - ): HttpClient = HttpClient { - - // Content negotiation with JSON (based on PingApiClient pattern) - install(ContentNegotiation) { - json(Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }) - } - } - - fun createClientWithBaseUrl(baseUrl: String): HttpClient { - return createClient(baseUrl) - } -} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/network/NetworkException.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/network/NetworkException.kt deleted file mode 100644 index 0cd3d3fb..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/network/NetworkException.kt +++ /dev/null @@ -1,164 +0,0 @@ -package at.mocode.clients.shared.network - -import at.mocode.clients.shared.domain.models.ApiError -import io.ktor.client.network.sockets.* -import io.ktor.client.plugins.* -import kotlinx.io.IOException - -/** - * Custom exceptions for network operations - */ -sealed class NetworkException( - message: String, - cause: Throwable? = null, - val apiError: ApiError -) : Exception(message, cause) { - - class ConnectionException( - message: String = "Connection failed", - cause: Throwable? = null - ) : NetworkException( - message = message, - cause = cause, - apiError = ApiError( - code = "CONNECTION_ERROR", - message = message, - details = mapOf("type" to "network_connectivity") - ) - ) - - class TimeoutException( - message: String = "Request timed out", - cause: Throwable? = null - ) : NetworkException( - message = message, - cause = cause, - apiError = ApiError( - code = "TIMEOUT_ERROR", - message = message, - details = mapOf("type" to "request_timeout") - ) - ) - - class ServerException( - statusCode: Int, - message: String = "Server error", - cause: Throwable? = null - ) : NetworkException( - message = message, - cause = cause, - apiError = ApiError( - code = "SERVER_ERROR", - message = message, - details = mapOf( - "type" to "server_error", - "status_code" to statusCode.toString() - ) - ) - ) - - class ClientException( - statusCode: Int, - message: String = "Client error", - cause: Throwable? = null - ) : NetworkException( - message = message, - cause = cause, - apiError = ApiError( - code = "CLIENT_ERROR", - message = message, - details = mapOf( - "type" to "client_error", - "status_code" to statusCode.toString() - ) - ) - ) - - class AuthenticationException( - message: String = "Authentication failed", - cause: Throwable? = null - ) : NetworkException( - message = message, - cause = cause, - apiError = ApiError( - code = "AUTHENTICATION_ERROR", - message = message, - details = mapOf("type" to "authentication_failure") - ) - ) - - class AuthorizationException( - message: String = "Authorization failed", - cause: Throwable? = null - ) : NetworkException( - message = message, - cause = cause, - apiError = ApiError( - code = "AUTHORIZATION_ERROR", - message = message, - details = mapOf("type" to "authorization_failure") - ) - ) - - class UnknownException( - message: String = "Unknown error occurred", - cause: Throwable? = null - ) : NetworkException( - message = message, - cause = cause, - apiError = ApiError( - code = "UNKNOWN_ERROR", - message = message, - details = mapOf("type" to "unknown_error") - ) - ) -} - -/** - * Extension function to convert various exceptions to NetworkException - */ -fun Throwable.toNetworkException(): NetworkException { - return when (this) { - is ConnectTimeoutException -> NetworkException.TimeoutException( - message = "Connection timeout: ${this.message}", - cause = this - ) - is SocketTimeoutException -> NetworkException.TimeoutException( - message = "Socket timeout: ${this.message}", - cause = this - ) - is ResponseException -> when (this.response.status.value) { - 401 -> NetworkException.AuthenticationException( - message = "Authentication required", - cause = this - ) - 403 -> NetworkException.AuthorizationException( - message = "Access forbidden", - cause = this - ) - in 400..499 -> NetworkException.ClientException( - statusCode = this.response.status.value, - message = "Client error: ${this.message}", - cause = this - ) - in 500..599 -> NetworkException.ServerException( - statusCode = this.response.status.value, - message = "Server error: ${this.message}", - cause = this - ) - else -> NetworkException.UnknownException( - message = "HTTP error: ${this.message}", - cause = this - ) - } - is IOException -> NetworkException.ConnectionException( - message = "Network connection failed: ${this.message}", - cause = this - ) - is NetworkException -> this - else -> NetworkException.UnknownException( - message = "Unexpected error: ${this.message}", - cause = this - ) - } -} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/network/NetworkUtils.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/network/NetworkUtils.kt deleted file mode 100644 index 770a9f77..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/network/NetworkUtils.kt +++ /dev/null @@ -1,217 +0,0 @@ -package at.mocode.clients.shared.network - -import at.mocode.clients.shared.data.repository.RepositoryResult -import at.mocode.clients.shared.domain.models.ApiError -import kotlinx.coroutines.delay -// Using platform-agnostic timestamp handling - -/** - * Simple timestamp provider for multiplatform compatibility - */ -expect fun currentTimeMillis(): Long - -/** - * Network utilities for handling retry logic and resilience - */ -object NetworkUtils { - - /** - * Retry configuration for network operations - */ - data class RetryConfig( - val maxAttempts: Int = 3, - val initialDelayMs: Long = 1000L, - val maxDelayMs: Long = 10000L, - val backoffMultiplier: Double = 2.0, - val retryableExceptions: Set = setOf( - "CONNECTION_ERROR", - "TIMEOUT_ERROR", - "SERVER_ERROR" - ) - ) - - /** - * Execute operation with retry logic - */ - suspend fun withRetry( - config: RetryConfig = RetryConfig(), - operation: suspend () -> RepositoryResult - ): RepositoryResult { - var lastError: ApiError? = null - var currentDelay = config.initialDelayMs - - repeat(config.maxAttempts) { attempt -> - try { - val result = operation() - - // Return success immediately - if (result.isSuccess()) { - return result - } - - // Check if the error is retryable - val error = result.getErrorOrNull() - if (error != null && shouldRetry(error, config)) { - lastError = error - - // Don't delay on the last attempt - if (attempt < config.maxAttempts - 1) { - delay(currentDelay) - currentDelay = minOf( - (currentDelay * config.backoffMultiplier).toLong(), - config.maxDelayMs - ) - } - } else { - // Non-retryable error, return immediately - return result - } - } catch (e: Exception) { - val networkException = e.toNetworkException() - lastError = networkException.apiError - - if (shouldRetry(networkException.apiError, config)) { - if (attempt < config.maxAttempts - 1) { - delay(currentDelay) - currentDelay = minOf( - (currentDelay * config.backoffMultiplier).toLong(), - config.maxDelayMs - ) - } - } else { - return RepositoryResult.Error(networkException.apiError) - } - } - } - - // All attempts exhausted, return last error - return RepositoryResult.Error( - lastError ?: ApiError( - code = "MAX_RETRIES_EXCEEDED", - message = "Maximum retry attempts exceeded" - ) - ) - } - - /** - * Check if an error should trigger a retry - */ - private fun shouldRetry(error: ApiError, config: RetryConfig): Boolean { - return config.retryableExceptions.contains(error.code) - } - - /** - * Network connectivity checker (simplified for shared module) - */ - object ConnectivityChecker { - private var isOnline: Boolean = true - private var lastCheckMillis: Long = 0L - - fun setOnlineStatus(online: Boolean) { - isOnline = online - lastCheckMillis = currentTimeMillis() - } - - fun isOnline(): Boolean = isOnline - - fun getLastCheckMillis(): Long = lastCheckMillis - - /** - * Simple connectivity test by attempting a lightweight operation - */ - suspend fun checkConnectivity(testOperation: suspend () -> Boolean): Boolean { - return try { - val result = testOperation() - setOnlineStatus(result) - result - } catch (_: Exception) { - setOnlineStatus(false) - false - } - } - } - - /** - * Circuit breaker pattern for network operations - */ - class CircuitBreaker( - private val failureThreshold: Int = 5, - private val recoveryTimeoutMs: Long = 60000L, - private val successThreshold: Int = 3 - ) { - private enum class State { CLOSED, OPEN, HALF_OPEN } - - private var state = State.CLOSED - private var failureCount = 0 - private var successCount = 0 - private var lastFailureTime = 0L - - suspend fun execute(operation: suspend () -> RepositoryResult): RepositoryResult { - when (state) { - State.OPEN -> { - if (currentTimeMillis() - lastFailureTime >= recoveryTimeoutMs) { - state = State.HALF_OPEN - successCount = 0 - } else { - return RepositoryResult.Error( - ApiError( - code = "CIRCUIT_BREAKER_OPEN", - message = "Circuit breaker is open, requests blocked" - ) - ) - } - } - State.HALF_OPEN -> { - // Allow limited requests to test recovery - } - State.CLOSED -> { - // Normal operation - } - } - - return try { - val result = operation() - - if (result.isSuccess()) { - onSuccess() - } else { - onFailure() - } - - result - } catch (e: Exception) { - onFailure() - val networkException = e.toNetworkException() - RepositoryResult.Error(networkException.apiError) - } - } - - private fun onSuccess() { - failureCount = 0 - - when (state) { - State.HALF_OPEN -> { - successCount++ - if (successCount >= successThreshold) { - state = State.CLOSED - } - } - else -> { - state = State.CLOSED - } - } - } - - private fun onFailure() { - failureCount++ - lastFailureTime = currentTimeMillis() - - if (failureCount >= failureThreshold) { - state = State.OPEN - } - } - - fun getState(): String = state.name - fun getFailureCount(): Int = failureCount - } -} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/presentation/actions/AppAction.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/presentation/actions/AppAction.kt deleted file mode 100644 index 0fe522fa..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/presentation/actions/AppAction.kt +++ /dev/null @@ -1,36 +0,0 @@ -package at.mocode.clients.shared.presentation.actions - -import at.mocode.clients.shared.domain.models.User -import at.mocode.clients.shared.domain.models.AuthToken - -sealed class AppAction { - // Auth Actions - sealed class Auth : AppAction() { - data class LoginStart(val username: String, val password: String) : Auth() - data class LoginSuccess(val user: User, val token: AuthToken) : Auth() - data class LoginFailure(val error: String) : Auth() - object Logout : Auth() - data class RefreshToken(val newToken: AuthToken) : Auth() - } - - // Navigation Actions - sealed class Navigation : AppAction() { - data class NavigateTo(val route: String) : Navigation() - object NavigateBack : Navigation() - data class UpdateHistory(val route: String) : Navigation() - } - - // UI Actions - sealed class UI : AppAction() { - object ToggleDarkMode : UI() - data class SetLoading(val isLoading: Boolean) : UI() - data class ShowNotification(val notification: at.mocode.clients.shared.presentation.state.Notification) : UI() - data class DismissNotification(val id: String) : UI() - } - - // Network Actions - sealed class Network : AppAction() { - data class SetOnlineStatus(val isOnline: Boolean) : Network() - data class UpdateLastSync(val timestamp: String) : Network() - } -} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/presentation/state/AppState.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/presentation/state/AppState.kt deleted file mode 100644 index 5974e3a8..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/presentation/state/AppState.kt +++ /dev/null @@ -1,55 +0,0 @@ -package at.mocode.clients.shared.presentation.state - -import at.mocode.clients.shared.domain.models.User -import at.mocode.clients.shared.domain.models.AuthToken -import kotlinx.serialization.Serializable - -@Serializable -data class AppState( - val auth: AuthState = AuthState(), - val navigation: NavigationState = NavigationState(), - val ui: UiState = UiState(), - val network: NetworkState = NetworkState() -) - -@Serializable -data class AuthState( - val isAuthenticated: Boolean = false, - val user: User? = null, - val token: AuthToken? = null, - val isLoading: Boolean = false, - val error: String? = null -) - -@Serializable -data class NavigationState( - val currentRoute: String = "/", - val history: List = emptyList(), - val canGoBack: Boolean = false -) - -@Serializable -data class UiState( - val isDarkMode: Boolean = false, - val isLoading: Boolean = false, - val notifications: List = emptyList() -) - -@Serializable -data class NetworkState( - val isOnline: Boolean = true, - val lastSync: String? = null -) - -@Serializable -data class Notification( - val id: String, - val title: String, - val message: String, - val type: NotificationType = NotificationType.INFO, - val timestamp: String -) - -enum class NotificationType { - INFO, SUCCESS, WARNING, ERROR -} diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/presentation/store/AppStore.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/presentation/store/AppStore.kt deleted file mode 100644 index fd30f24f..00000000 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/presentation/store/AppStore.kt +++ /dev/null @@ -1,137 +0,0 @@ -package at.mocode.clients.shared.presentation.store - -import at.mocode.clients.shared.presentation.state.AppState -import at.mocode.clients.shared.presentation.actions.AppAction -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -class AppStore( - private val dispatcher: CoroutineDispatcher = Dispatchers.Main -) { - private val scope = CoroutineScope(SupervisorJob() + dispatcher) - private val _state = MutableStateFlow(AppState()) - - val state: StateFlow = _state.asStateFlow() - - fun dispatch(action: AppAction) { - scope.launch { - val currentState = _state.value - val newState = reduce(currentState, action) - _state.value = newState - - // Handle side effects - handleSideEffect(action, newState) - } - } - - private fun reduce(currentState: AppState, action: AppAction): AppState { - return when (action) { - is AppAction.Auth -> currentState.copy( - auth = reduceAuth(currentState.auth, action) - ) - is AppAction.Navigation -> currentState.copy( - navigation = reduceNavigation(currentState.navigation, action) - ) - is AppAction.UI -> currentState.copy( - ui = reduceUI(currentState.ui, action) - ) - is AppAction.Network -> currentState.copy( - network = reduceNetwork(currentState.network, action) - ) - } - } - - private fun reduceAuth(currentAuth: at.mocode.clients.shared.presentation.state.AuthState, action: AppAction.Auth): at.mocode.clients.shared.presentation.state.AuthState { - return when (action) { - is AppAction.Auth.LoginStart -> currentAuth.copy( - isLoading = true, - error = null - ) - is AppAction.Auth.LoginSuccess -> currentAuth.copy( - isAuthenticated = true, - user = action.user, - token = action.token, - isLoading = false, - error = null - ) - is AppAction.Auth.LoginFailure -> currentAuth.copy( - isAuthenticated = false, - user = null, - token = null, - isLoading = false, - error = action.error - ) - is AppAction.Auth.Logout -> at.mocode.clients.shared.presentation.state.AuthState() - is AppAction.Auth.RefreshToken -> currentAuth.copy( - token = action.newToken - ) - } - } - - private fun reduceNavigation(currentNav: at.mocode.clients.shared.presentation.state.NavigationState, action: AppAction.Navigation): at.mocode.clients.shared.presentation.state.NavigationState { - return when (action) { - is AppAction.Navigation.NavigateTo -> currentNav.copy( - currentRoute = action.route, - history = currentNav.history + currentNav.currentRoute, - canGoBack = true - ) - is AppAction.Navigation.NavigateBack -> { - val newHistory = currentNav.history.dropLast(1) - currentNav.copy( - currentRoute = newHistory.lastOrNull() ?: "/", - history = newHistory, - canGoBack = newHistory.isNotEmpty() - ) - } - is AppAction.Navigation.UpdateHistory -> currentNav.copy( - currentRoute = action.route - ) - } - } - - private fun reduceUI(currentUI: at.mocode.clients.shared.presentation.state.UiState, action: AppAction.UI): at.mocode.clients.shared.presentation.state.UiState { - return when (action) { - is AppAction.UI.ToggleDarkMode -> currentUI.copy( - isDarkMode = !currentUI.isDarkMode - ) - is AppAction.UI.SetLoading -> currentUI.copy( - isLoading = action.isLoading - ) - is AppAction.UI.ShowNotification -> currentUI.copy( - notifications = currentUI.notifications + action.notification - ) - is AppAction.UI.DismissNotification -> currentUI.copy( - notifications = currentUI.notifications.filter { it.id != action.id } - ) - } - } - - private fun reduceNetwork(currentNetwork: at.mocode.clients.shared.presentation.state.NetworkState, action: AppAction.Network): at.mocode.clients.shared.presentation.state.NetworkState { - return when (action) { - is AppAction.Network.SetOnlineStatus -> currentNetwork.copy( - isOnline = action.isOnline - ) - is AppAction.Network.UpdateLastSync -> currentNetwork.copy( - lastSync = action.timestamp - ) - } - } - - private suspend fun handleSideEffect(action: AppAction, newState: AppState) { - when (action) { - is AppAction.Auth.LoginSuccess -> { - // Auto-save token to local storage - // TODO: Implement storage - } - is AppAction.Auth.Logout -> { - // Clear local storage - // TODO: Implement storage cleanup - } - else -> { /* No side effects */ } - } - } - - fun cleanup() { - scope.cancel() - } -} diff --git a/clients/shared/src/commonTest/kotlin/at/mocode/clients/shared/presentation/store/AppStoreTest.kt b/clients/shared/src/commonTest/kotlin/at/mocode/clients/shared/presentation/store/AppStoreTest.kt deleted file mode 100644 index 032825ad..00000000 --- a/clients/shared/src/commonTest/kotlin/at/mocode/clients/shared/presentation/store/AppStoreTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -package at.mocode.clients.shared.presentation.store - -import at.mocode.clients.shared.domain.models.User -import at.mocode.clients.shared.domain.models.AuthToken -import at.mocode.clients.shared.presentation.actions.AppAction -import kotlinx.coroutines.Dispatchers -import kotlin.test.* - -class AppStoreTest { - - @Test - fun `store should be created successfully`() { - val store = AppStore(Dispatchers.Unconfined) - assertNotNull(store) - store.cleanup() - } - - @Test - fun `auth actions should update state`() { - val store = AppStore(Dispatchers.Unconfined) - - // Test login start action - store.dispatch(AppAction.Auth.LoginStart("testuser", "password")) - - // Test login success - val user = User("1", "test", "test@example.com", "Test", "User") - val token = AuthToken("access", "refresh", 3600) - store.dispatch(AppAction.Auth.LoginSuccess(user, token)) - - // Test logout - store.dispatch(AppAction.Auth.Logout) - - store.cleanup() - assertTrue(true) // Basic test to verify actions don't throw exceptions - } - - @Test - fun `navigation actions should work`() { - val store = AppStore(Dispatchers.Unconfined) - - store.dispatch(AppAction.Navigation.NavigateTo("/dashboard")) - store.dispatch(AppAction.Navigation.NavigateBack) - - store.cleanup() - assertTrue(true) - } - - @Test - fun `ui actions should work`() { - val store = AppStore(Dispatchers.Unconfined) - - store.dispatch(AppAction.UI.ToggleDarkMode) - store.dispatch(AppAction.UI.SetLoading(true)) - - store.cleanup() - assertTrue(true) - } - - @Test - fun `network actions should work`() { - val store = AppStore(Dispatchers.Unconfined) - - store.dispatch(AppAction.Network.SetOnlineStatus(false)) - store.dispatch(AppAction.Network.UpdateLastSync("2024-01-01T12:00:00Z")) - - store.cleanup() - assertTrue(true) - } -} diff --git a/clients/shared/src/commonTest/kotlin/at/mocode/clients/shared/test/TestUtils.kt b/clients/shared/src/commonTest/kotlin/at/mocode/clients/shared/test/TestUtils.kt deleted file mode 100644 index af4868f0..00000000 --- a/clients/shared/src/commonTest/kotlin/at/mocode/clients/shared/test/TestUtils.kt +++ /dev/null @@ -1,18 +0,0 @@ -package at.mocode.clients.shared.test - -import kotlin.test.AfterTest -import kotlin.test.BeforeTest - -expect fun runBlockingTest(block: suspend () -> Unit) - -abstract class BaseTest { - @BeforeTest - fun setupTest() { - // Set up a common test environment - } - - @AfterTest - fun teardownTest() { - // Cleanup test environment - } -} diff --git a/clients/shared/src/jsMain/kotlin/at/mocode/clients/shared/network/NetworkUtilsJs.kt b/clients/shared/src/jsMain/kotlin/at/mocode/clients/shared/network/NetworkUtilsJs.kt deleted file mode 100644 index 32e55ff7..00000000 --- a/clients/shared/src/jsMain/kotlin/at/mocode/clients/shared/network/NetworkUtilsJs.kt +++ /dev/null @@ -1,5 +0,0 @@ -package at.mocode.clients.shared.network - -import kotlin.js.Date - -actual fun currentTimeMillis(): Long = Date.now().toLong() diff --git a/clients/shared/src/jsTest/kotlin/at/mocode/clients/shared/test/TestUtilsJs.kt b/clients/shared/src/jsTest/kotlin/at/mocode/clients/shared/test/TestUtilsJs.kt deleted file mode 100644 index 2ee61d31..00000000 --- a/clients/shared/src/jsTest/kotlin/at/mocode/clients/shared/test/TestUtilsJs.kt +++ /dev/null @@ -1,10 +0,0 @@ -package at.mocode.clients.shared.test - -import kotlinx.coroutines.* - -@OptIn(DelicateCoroutinesApi::class) -actual fun runBlockingTest(block: suspend () -> Unit) { - GlobalScope.promise { - block() - } -} diff --git a/clients/shared/src/jvmMain/kotlin/at/mocode/clients/shared/network/NetworkUtilsJvm.kt b/clients/shared/src/jvmMain/kotlin/at/mocode/clients/shared/network/NetworkUtilsJvm.kt deleted file mode 100644 index bc437617..00000000 --- a/clients/shared/src/jvmMain/kotlin/at/mocode/clients/shared/network/NetworkUtilsJvm.kt +++ /dev/null @@ -1,3 +0,0 @@ -package at.mocode.clients.shared.network - -actual fun currentTimeMillis(): Long = System.currentTimeMillis() diff --git a/clients/shared/src/jvmTest/kotlin/at/mocode/clients/shared/test/TestUtilsJvm.kt b/clients/shared/src/jvmTest/kotlin/at/mocode/clients/shared/test/TestUtilsJvm.kt deleted file mode 100644 index 68ad4a43..00000000 --- a/clients/shared/src/jvmTest/kotlin/at/mocode/clients/shared/test/TestUtilsJvm.kt +++ /dev/null @@ -1,9 +0,0 @@ -package at.mocode.clients.shared.test - -import kotlinx.coroutines.test.* - -actual fun runBlockingTest(block: suspend () -> Unit) { - runTest { - block() - } -} diff --git a/clients/shared/src/wasmJsMain/kotlin/at/mocode/clients/shared/network/NetworkUtilsWasm.kt b/clients/shared/src/wasmJsMain/kotlin/at/mocode/clients/shared/network/NetworkUtilsWasm.kt deleted file mode 100644 index 1234bb3e..00000000 --- a/clients/shared/src/wasmJsMain/kotlin/at/mocode/clients/shared/network/NetworkUtilsWasm.kt +++ /dev/null @@ -1,11 +0,0 @@ -package at.mocode.clients.shared.network - -// WASM implementation using a simple counter-approach -// Since we don't have direct access to system time in WASM, -// we'll use a monotonic counter for relative timing -private var wasmTimeCounter: Long = 0L - -actual fun currentTimeMillis(): Long { - wasmTimeCounter += 1 - return wasmTimeCounter -} diff --git a/clients/shared/src/wasmJsTest/kotlin/at/mocode/clients/shared/test/TestUtilsWasm.kt b/clients/shared/src/wasmJsTest/kotlin/at/mocode/clients/shared/test/TestUtilsWasm.kt deleted file mode 100644 index ba4863e5..00000000 --- a/clients/shared/src/wasmJsTest/kotlin/at/mocode/clients/shared/test/TestUtilsWasm.kt +++ /dev/null @@ -1,11 +0,0 @@ -package at.mocode.clients.shared.test - -import kotlinx.coroutines.* - -@OptIn(DelicateCoroutinesApi::class, ExperimentalWasmJsInterop::class) -actual fun runBlockingTest(block: suspend () -> Unit) { - // WASM-JS uses the same approach as regular JS - GlobalScope.promise { - block() - } -} diff --git a/compose.hardcoded.yaml b/compose.hardcoded.yaml index edf08806..9f8138ba 100644 --- a/compose.hardcoded.yaml +++ b/compose.hardcoded.yaml @@ -141,6 +141,43 @@ services: networks: - meldestelle-network + # --- CLIENTS: WEB APP (Kotlin/JS, no WASM) --- + web-app: + build: + context: . + dockerfile: dockerfiles/clients/web-app/Dockerfile + args: + GRADLE_VERSION: 9.1.0 + JAVA_VERSION: 21 + NODE_VERSION: 22.21.0 + NGINX_IMAGE_TAG: 1.28.0-alpine + WEB_BUILD_PROFILE: dev + container_name: meldestelle-web-app + restart: unless-stopped + ports: + - "4000:4000" + depends_on: + - api-gateway + networks: + - meldestelle-network + + # --- CLIENTS: DESKTOP APP (VNC + noVNC) --- + desktop-app: + build: + context: . + dockerfile: dockerfiles/clients/desktop-app/Dockerfile + container_name: meldestelle-desktop-app + restart: unless-stopped + environment: + - API_BASE_URL=http://api-gateway:8081 + ports: + - "5901:5901" # VNC + - "6080:6080" # noVNC + depends_on: + - api-gateway + networks: + - meldestelle-network + volumes: postgres-data: pgadmin-data: diff --git a/compose.yaml b/compose.yaml index 0813225b..4d3e2fc6 100644 --- a/compose.yaml +++ b/compose.yaml @@ -29,7 +29,7 @@ services: - postgres redis: - image: redis:7-alpine + image: redis:8.4-alpine container_name: ${COMPOSE_PROJECT_NAME}-redis restart: unless-stopped ports: @@ -105,7 +105,7 @@ services: - pgadmin prometheus: - image: prom/prometheus:v2.54.1 + image: prom/prometheus:v3.7.3 container_name: ${COMPOSE_PROJECT_NAME}-prometheus restart: unless-stopped ports: @@ -128,7 +128,7 @@ services: - prometheus grafana: - image: grafana/grafana:11.3.0 + image: grafana/grafana:12.3 container_name: ${COMPOSE_PROJECT_NAME}-grafana restart: unless-stopped environment: @@ -157,7 +157,7 @@ services: # ========================================== consul: - image: hashicorp/consul:1.15 + image: hashicorp/consul:1.22.1 container_name: ${COMPOSE_PROJECT_NAME}-consul restart: unless-stopped ports: @@ -182,7 +182,7 @@ services: GRADLE_VERSION: 9.1.0 JAVA_VERSION: 21 VERSION: 1.0.0 - BUILD_DATE: "2025-11-25" + BUILD_DATE: "2025-11-29" container_name: ${COMPOSE_PROJECT_NAME}-gateway restart: no ports: @@ -230,7 +230,7 @@ services: GRADLE_VERSION: 9.1.0 JAVA_VERSION: 21 VERSION: 1.0.0 - BUILD_DATE: "2025-11-21" + BUILD_DATE: "2025-11-29" container_name: ${COMPOSE_PROJECT_NAME}-ping-service restart: no # "${RESTART_POLICY:-unless-stopped}" ports: @@ -246,7 +246,7 @@ services: SPRING_CLOUD_CONSUL_PORT: 8500 SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME: ping-service - # --- DATENBANK VERBINDUNG (Das hat gefehlt!) --- + # --- DATENBANK VERBINDUNG --- # Wir nutzen die Container-Namen aus deiner .env Variable SPRING_DATASOURCE_URL: jdbc:postgresql://${COMPOSE_PROJECT_NAME}-postgres:5432/${POSTGRES_DB} SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER} @@ -254,7 +254,7 @@ services: # WICHTIG: Wir wollen nur validieren, nichts erstellen. SPRING_JPA_HIBERNATE_DDL_AUTO: validate - # --- REDIS (DAS HAT GEFEHLT!) --- + # --- REDIS --- # Wir nutzen den Service-Namen, genau wie bei Postgres SPRING_DATA_REDIS_HOST: ${COMPOSE_PROJECT_NAME}-redis SPRING_DATA_REDIS_PORT: 6379 @@ -270,6 +270,50 @@ services: aliases: - ping-service + # ========================================== + # CLIENT APPLICATIONS + # ========================================== + web-app: + build: + context: . + dockerfile: dockerfiles/clients/web-app/Dockerfile + args: + GRADLE_VERSION: ${DOCKER_GRADLE_VERSION:-9.1.0} + JAVA_VERSION: ${DOCKER_JAVA_VERSION:-21} + NODE_VERSION: ${DOCKER_NODE_VERSION:-22.21.0} + NGINX_IMAGE_TAG: ${DOCKER_NGINX_VERSION:-1.28.0-alpine} + WEB_BUILD_PROFILE: ${WEB_BUILD_PROFILE:-dev} + container_name: ${COMPOSE_PROJECT_NAME}-web-app + restart: unless-stopped + ports: + - "${WEB_APP_PORT}" + depends_on: + api-gateway: + condition: service_started + networks: + meldestelle-network: + aliases: + - web-app + + desktop-app: + build: + context: . + dockerfile: dockerfiles/clients/desktop-app/Dockerfile + container_name: ${COMPOSE_PROJECT_NAME}-desktop-app + restart: unless-stopped + environment: + API_BASE_URL: http://api-gateway:8081 + ports: + - "${DESKTOP_APP_VNC_PORT}" + - "${DESKTOP_APP_NOVNC_PORT}" + depends_on: + api-gateway: + condition: service_started + networks: + meldestelle-network: + aliases: + - desktop-app + volumes: postgres-data: pgadmin-data: diff --git a/config/env/clients/web-app.env b/config/env/clients/web-app.env deleted file mode 100644 index b5c9359b..00000000 --- a/config/env/clients/web-app.env +++ /dev/null @@ -1,8 +0,0 @@ -# Optional Client Override – Web App -# Diese Datei wird zusätzlich zu config/env/.env geladen. -# Nur befüllen, wenn die Web-App abweichende Runtime-Werte benötigt. -# Beispiel-Overrides (auskommentiert lassen, falls nicht benötigt): -# -# WEB_APP_PORT=4001 -# NODE_ENV=development -# APP_TITLE=Meldestelle (Dev) diff --git a/config/env/infrastructure/api-gateway.env b/config/env/infrastructure/api-gateway.env deleted file mode 100644 index 33a1c0ac..00000000 --- a/config/env/infrastructure/api-gateway.env +++ /dev/null @@ -1,8 +0,0 @@ -# Optional Infrastructure Override – API Gateway -# Diese Datei wird zusätzlich zu config/env/.env geladen. -# Nur befüllen, wenn das Gateway abweichende Runtime-Werte benötigt. -# Beispiel-Overrides (auskommentiert lassen, falls nicht benötigt): -# -# GATEWAY_PORT=8081 -# SPRING_PROFILES_ACTIVE=docker,keycloak -# LOGGING_LEVEL_ROOT=DEBUG diff --git a/config/env/services/events-service.env b/config/env/services/events-service.env deleted file mode 100644 index bd3ad54b..00000000 --- a/config/env/services/events-service.env +++ /dev/null @@ -1,8 +0,0 @@ -# Optional Service Override – Events Service -# Diese Datei wird zusätzlich zu config/env/.env geladen. -# Nur befüllen, wenn der Events-Service abweichende Runtime-Werte benötigt. -# Beispiel-Overrides (auskommentiert lassen, falls nicht benötigt): -# -# SERVER_PORT=8085 -# LOGGING_LEVEL_ROOT=DEBUG -# DEBUG=true diff --git a/config/env/services/horses-service.env b/config/env/services/horses-service.env deleted file mode 100644 index 5de37875..00000000 --- a/config/env/services/horses-service.env +++ /dev/null @@ -1,8 +0,0 @@ -# Optional Service Override – Horses Service -# Diese Datei wird zusätzlich zu config/env/.env geladen. -# Nur befüllen, wenn der Horses-Service abweichende Runtime-Werte benötigt. -# Beispiel-Overrides (auskommentiert lassen, falls nicht benötigt): -# -# SERVER_PORT=8084 -# LOGGING_LEVEL_ROOT=DEBUG -# DEBUG=true diff --git a/config/env/services/masterdata-service.env b/config/env/services/masterdata-service.env deleted file mode 100644 index 7969503a..00000000 --- a/config/env/services/masterdata-service.env +++ /dev/null @@ -1,8 +0,0 @@ -# Optional Service Override – Masterdata Service -# Diese Datei wird zusätzlich zu config/env/.env geladen. -# Nur befüllen, wenn der Masterdata-Service abweichende Runtime-Werte benötigt. -# Beispiel-Overrides (auskommentiert lassen, falls nicht benötigt): -# -# SERVER_PORT=8086 -# LOGGING_LEVEL_ROOT=DEBUG -# DEBUG=true diff --git a/config/env/services/members-service.env b/config/env/services/members-service.env deleted file mode 100644 index 61a5e9c3..00000000 --- a/config/env/services/members-service.env +++ /dev/null @@ -1,8 +0,0 @@ -# Optional Service Override – Members Service -# Diese Datei wird zusätzlich zu config/env/.env geladen. -# Nur befüllen, wenn der Members-Service abweichende Runtime-Werte benötigt. -# Beispiel-Overrides (auskommentiert lassen, falls nicht benötigt): -# -# SERVER_PORT=8083 -# LOGGING_LEVEL_ROOT=DEBUG -# DEBUG=true diff --git a/config/env/services/ping-service.env b/config/env/services/ping-service.env deleted file mode 100644 index 153620c9..00000000 --- a/config/env/services/ping-service.env +++ /dev/null @@ -1,8 +0,0 @@ -# Optional Service Override – Ping Service -# Diese Datei wird zusätzlich zu config/env/.env geladen. -# Nur befüllen, wenn der Ping-Service abweichende Runtime-Werte benötigt. -# Beispiel-Overrides (auskommentiert lassen, falls nicht benötigt): -# -# SERVER_PORT=8082 -# LOGGING_LEVEL_ROOT=DEBUG -# DEBUG=true diff --git a/core/README.md b/core/README.md deleted file mode 100644 index 088392dc..00000000 --- a/core/README.md +++ /dev/null @@ -1 +0,0 @@ -# Core\n\nMinimal placeholder README. See docs/index.md for project documentation diff --git a/core/core-domain/build.gradle.kts b/core/core-domain/build.gradle.kts index 8cf8dcb4..77fd741f 100644 --- a/core/core-domain/build.gradle.kts +++ b/core/core-domain/build.gradle.kts @@ -1,71 +1,73 @@ plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) } kotlin { - jvmToolchain(21) + jvmToolchain(21) - jvm { - compilerOptions { - freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") - } + jvm { + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + } + } + + js(IR) { + browser { + testTask { + enabled = false + } + } + } + + sourceSets { + // Opt-in to experimental Kotlin UUID API across all source sets + all { + languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi") + // Opt-in für kotlin.time.ExperimentalTime projektweit, solange Teile noch experimentell sind + languageSettings.optIn("kotlin.time.ExperimentalTime") } - js(IR) { - browser { - testTask { - enabled = false - } - } - } + commonMain.dependencies { + // Core dependencies (that aren't included in platform-dependencies) + // Note: core-domain should NOT depend on core-utils to avoid circular dependencies + // core-utils depends on core-domain, not the other way around - sourceSets { - // Opt-in to experimental Kotlin UUID API across all source sets - all { - languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi") - } - - commonMain.dependencies { - // Core dependencies (that aren't included in platform-dependencies) - // Note: core-domain should NOT depend on core-utils to avoid circular dependencies - // core-utils depends on core-domain, not the other way around - - // Serialization and date-time for commonMain - api(libs.kotlinx.serialization.json) - api(libs.kotlinx.datetime) - - } - - commonTest.dependencies { - implementation(libs.kotlin.test) - } - - jsMain.dependencies { - api(libs.kotlinx.coroutines.core) - } - - jsTest.dependencies { - implementation(libs.kotlin.test) - } - - jvmMain.dependencies { - // Fachliches Domain-Modul: keine technischen Abhängigkeiten hier hinterlegen. - // Falls in Zukunft JVM-spezifische, fachlich neutrale Ergänzungen nötig sind, - // bitte bewusst und minimal hinzufügen. - } - - jvmTest.dependencies { - // implementation(kotlin("test-junit5")) - implementation(libs.junit.jupiter.api) - implementation(libs.mockk) - implementation(projects.platform.platformTesting) - implementation(libs.bundles.testing.jvm) - } + // Serialization and date-time for commonMain + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.datetime) } + + commonTest.dependencies { + implementation(libs.kotlin.test) + } + + jsMain.dependencies { + api(libs.kotlinx.coroutines.core) + } + + jsTest.dependencies { + implementation(libs.kotlin.test) + } + + jvmMain.dependencies { + // Fachliches Domain-Modul: keine technischen Abhängigkeiten hier hinterlegen. + // Falls in Zukunft JVM-spezifische, fachlich neutrale Ergänzungen nötig sind, + // bitte bewusst und minimal hinzufügen. + } + + jvmTest.dependencies { + // implementation(kotlin("test-junit5")) + implementation(libs.junit.jupiter.api) + implementation(libs.mockk) + implementation(projects.platform.platformTesting) + implementation(libs.bundles.testing.jvm) + } + + } } tasks.named("jvmTest") { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/event/DomainEvent.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/event/DomainEvent.kt index 0c09eb4d..c30d04cd 100644 --- a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/event/DomainEvent.kt +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/event/DomainEvent.kt @@ -1,11 +1,11 @@ @file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + package at.mocode.core.domain.event import at.mocode.core.domain.model.* -import at.mocode.core.domain.serialization.KotlinInstantSerializer +import at.mocode.core.domain.serialization.KotlinxInstantSerializer import kotlinx.serialization.Serializable -import kotlin.time.Clock -import kotlin.time.ExperimentalTime +import kotlin.time.Clock as KtClock import kotlin.time.Instant import kotlin.uuid.Uuid @@ -13,73 +13,71 @@ import kotlin.uuid.Uuid * Basis-Interface für alle Domain-Events im System. * Ein Domain-Event beschreibt ein fachlich relevantes Ereignis, das stattgefunden hat. */ -@OptIn(ExperimentalTime::class) interface DomainEvent { - val eventId: EventId - val aggregateId: AggregateId - val eventType: EventType - val timestamp: Instant - val version: EventVersion - val correlationId: CorrelationId? - val causationId: CausationId? + val eventId: EventId + val aggregateId: AggregateId + val eventType: EventType + val timestamp: Instant + val version: EventVersion + val correlationId: CorrelationId? + val causationId: CausationId? } /** * Abstrakte Basisklasse für Domain-Events, um Boilerplate zu reduzieren. */ @Serializable -@OptIn(ExperimentalTime::class) abstract class BaseDomainEvent( - override val aggregateId: AggregateId, - override val eventType: EventType, - override val version: EventVersion, - override val eventId: EventId = EventId(Uuid.random()), - @Serializable(with = KotlinInstantSerializer::class) - override val timestamp: Instant, - override val correlationId: CorrelationId? = null, - override val causationId: CausationId? = null + override val aggregateId: AggregateId, + override val eventType: EventType, + override val version: EventVersion, + override val eventId: EventId = EventId(Uuid.random()), + + @Serializable(with = KotlinxInstantSerializer::class) + override val timestamp: Instant, + override val correlationId: CorrelationId? = null, + override val causationId: CausationId? = null ) : DomainEvent { - constructor( - aggregateId: AggregateId, - eventType: EventType, - version: EventVersion, - eventId: EventId = EventId(Uuid.random()), - correlationId: CorrelationId? = null, - causationId: CausationId? = null - ) : this( - aggregateId = aggregateId, - eventType = eventType, - version = version, - eventId = eventId, - timestamp = createTimestamp(), - correlationId = correlationId, - causationId = causationId - ) + constructor( + aggregateId: AggregateId, + eventType: EventType, + version: EventVersion, + eventId: EventId = EventId(Uuid.random()), + correlationId: CorrelationId? = null, + causationId: CausationId? = null + ) : this( + aggregateId = aggregateId, + eventType = eventType, + version = version, + eventId = eventId, + timestamp = createTimestamp(), + correlationId = correlationId, + causationId = causationId + ) - companion object { - @OptIn(ExperimentalTime::class) - private fun createTimestamp(): Instant = Clock.System.now() - } + companion object { + private fun createTimestamp(): Instant = Instant.parse(KtClock.System.now().toString()) + } } /** * Schnittstelle für einen Publisher, der Domain-Events veröffentlichen kann. */ interface DomainEventPublisher { - suspend fun publish(event: DomainEvent) - suspend fun publishAll(events: List) + suspend fun publish(event: DomainEvent) + suspend fun publishAll(events: List) } /** * Schnittstelle für einen Handler, der auf bestimmte Domain-Events reagieren kann. */ interface DomainEventHandler { - suspend fun handle(event: T) - fun canHandle(eventType: EventType): Boolean + suspend fun handle(event: T) + fun canHandle(eventType: EventType): Boolean - /** - * Rückwärtskompatible Methode für String-basierte Prüfung des Event-Typs. - */ - fun canHandle(eventType: String): Boolean = canHandle(EventType(eventType)) + /** + * Rückwärtskompatible Methode für String-basierte Prüfung des Event-Typs. + */ + fun canHandle(eventType: String): Boolean = canHandle(EventType(eventType)) } diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/BaseDto.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/BaseDto.kt index 93961da0..3a95c7ef 100644 --- a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/BaseDto.kt +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/BaseDto.kt @@ -1,6 +1,6 @@ package at.mocode.core.domain.model -import at.mocode.core.domain.serialization.KotlinInstantSerializer +import at.mocode.core.domain.serialization.KotlinxInstantSerializer import kotlinx.serialization.Serializable import kotlin.time.Clock import kotlin.time.ExperimentalTime @@ -15,15 +15,14 @@ interface BaseDto * Basis-DTO für Domänen-Entitäten mit eindeutiger ID und Audit-Zeitstempeln. */ @Serializable -@OptIn(ExperimentalTime::class) abstract class EntityDto : BaseDto { - abstract val id: EntityId + abstract val id: EntityId - @Serializable(with = KotlinInstantSerializer::class) - abstract val createdAt: Instant + @Serializable(with = KotlinxInstantSerializer::class) + abstract val createdAt: Instant - @Serializable(with = KotlinInstantSerializer::class) - abstract val updatedAt: Instant + @Serializable(with = KotlinxInstantSerializer::class) + abstract val updatedAt: Instant } /** @@ -31,57 +30,63 @@ abstract class EntityDto : BaseDto { */ @Serializable data class ErrorDto( - val code: ErrorCode, - val message: String, - val field: String? = null + val code: ErrorCode, + val message: String, + val field: String? = null ) : BaseDto /** * Standardisierte Hülle für API-Antworten mit einheitlicher Struktur. */ @Serializable -@OptIn(ExperimentalTime::class) data class ApiResponse( - val data: T?, - val success: Boolean, - val errors: List = emptyList(), - @Serializable(with = KotlinInstantSerializer::class) - val timestamp: Instant + val data: T?, + val success: Boolean, + val errors: List = emptyList(), + @Serializable(with = KotlinxInstantSerializer::class) + val timestamp: Instant ) { - companion object { - @OptIn(ExperimentalTime::class) - fun success(data: T): ApiResponse { - return ApiResponse(data = data, success = true, timestamp = Clock.System.now()) - } + companion object { + @OptIn(ExperimentalTime::class) + fun success(data: T): ApiResponse = + ApiResponse( + data = data, + success = true, + timestamp = Instant.parse(Clock.System.now().toString()) + ) - @OptIn(ExperimentalTime::class) - fun error( - code: ErrorCode, - message: String, - field: String? = null - ): ApiResponse { - return ApiResponse( - data = null, - success = false, - errors = listOf(ErrorDto(code = code, message = message, field = field)), - timestamp = Clock.System.now() - ) - } - - @OptIn(ExperimentalTime::class) - fun error( - code: String, - message: String, - field: String? = null - ): ApiResponse { - return error(ErrorCode(code), message, field) - } - - @OptIn(ExperimentalTime::class) - fun error(errors: List): ApiResponse { - return ApiResponse(data = null, success = false, errors = errors, timestamp = Clock.System.now()) - } + @OptIn(ExperimentalTime::class) + fun error( + code: ErrorCode, + message: String, + field: String? = null + ): ApiResponse { + return ApiResponse( + data = null, + success = false, + errors = listOf(ErrorDto(code = code, message = message, field = field)), + timestamp = Instant.parse(Clock.System.now().toString()) + ) } + + fun error( + code: String, + message: String, + field: String? = null + ): ApiResponse { + return error(ErrorCode(code), message, field) + } + + @OptIn(ExperimentalTime::class) + fun error(errors: List): ApiResponse { + return ApiResponse( + data = null, + success = false, + errors = errors, + timestamp = Instant.parse(Clock.System.now().toString()) + ) + } + } } /** @@ -89,37 +94,37 @@ data class ApiResponse( */ @Serializable data class PagedResponse( - val content: List, - val page: PageNumber, - val size: PageSize, - val totalElements: Long, - val totalPages: Int, - val hasNext: Boolean, - val hasPrevious: Boolean + val content: List, + val page: PageNumber, + val size: PageSize, + val totalElements: Long, + val totalPages: Int, + val hasNext: Boolean, + val hasPrevious: Boolean ) { - companion object { - /** - * Erzeugt eine PagedResponse mit Rückwärtskompatibilität für einfache Int-Werte. - * Nützlich, wenn Aufrufer noch keine PageNumber/PageSize verwenden. - */ - fun create( - content: List, - page: Int, - size: Int, - totalElements: Long, - totalPages: Int, - hasNext: Boolean, - hasPrevious: Boolean - ): PagedResponse { - return PagedResponse( - content = content, - page = PageNumber(page), - size = PageSize(size), - totalElements = totalElements, - totalPages = totalPages, - hasNext = hasNext, - hasPrevious = hasPrevious - ) - } + companion object { + /** + * Erzeugt eine PagedResponse mit Rückwärtskompatibilität für einfache Int-Werte. + * Nützlich, wenn Aufrufer noch keine PageNumber/PageSize verwenden. + */ + fun create( + content: List, + page: Int, + size: Int, + totalElements: Long, + totalPages: Int, + hasNext: Boolean, + hasPrevious: Boolean + ): PagedResponse { + return PagedResponse( + content = content, + page = PageNumber(page), + size = PageSize(size), + totalElements = totalElements, + totalPages = totalPages, + hasNext = hasNext, + hasPrevious = hasPrevious + ) } + } } diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/Enums.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/Enums.kt index d6ed8cbe..19cca6bb 100644 --- a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/Enums.kt +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/Enums.kt @@ -12,10 +12,10 @@ import kotlinx.serialization.Serializable */ @Serializable enum class DatenQuelleE { - MANUELL, - IMPORT_ZNS, - SYSTEM_GENERATED, - IMPORT_API + MANUELL, + IMPORT_ZNS, + SYSTEM_GENERATED, + IMPORT_API } /** @@ -23,11 +23,11 @@ enum class DatenQuelleE { */ @Serializable enum class StatusE { - AKTIV, - INAKTIV, - ENTWURF, - ARCHIVIERT, - GELOESCHT + AKTIV, + INAKTIV, + ENTWURF, + ARCHIVIERT, + GELOESCHT } /** @@ -35,10 +35,10 @@ enum class StatusE { */ @Serializable enum class PrioritaetE { - NIEDRIG, - NORMAL, - HOCH, - KRITISCH + NIEDRIG, + NORMAL, + HOCH, + KRITISCH } /** @@ -46,11 +46,11 @@ enum class PrioritaetE { */ @Serializable enum class BenutzerRolleE { - ADMIN, - BENUTZER, - MODERATOR, - GAST, - SYSTEM + ADMIN, + BENUTZER, + MODERATOR, + GAST, + SYSTEM } /** @@ -58,11 +58,11 @@ enum class BenutzerRolleE { */ @Serializable enum class VerifikationsStatusE { - NICHT_VERIFIZIERT, - IN_PRUEFUNG, - VERIFIZIERT, - ABGELEHNT, - KORREKTUR_ERFORDERLICH + NICHT_VERIFIZIERT, + IN_PRUEFUNG, + VERIFIZIERT, + ABGELEHNT, + KORREKTUR_ERFORDERLICH } /** @@ -70,10 +70,10 @@ enum class VerifikationsStatusE { */ @Serializable enum class BearbeitungsStatusE { - OFFEN, - IN_BEARBEITUNG, - WARTEND, - ABGESCHLOSSEN, - ABGEBROCHEN, - FEHLER + OFFEN, + IN_BEARBEITUNG, + WARTEND, + ABGESCHLOSSEN, + ABGEBROCHEN, + FEHLER } diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/ErrorCodes.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/ErrorCodes.kt new file mode 100644 index 00000000..6f98abbb --- /dev/null +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/ErrorCodes.kt @@ -0,0 +1,16 @@ +package at.mocode.core.domain.model + +/** + * Zentrale Sammlung der standardisierten Fehlercodes der Anwendung. + * Dient als Single-Source-of-Truth, um Inkonsistenzen zu vermeiden. + */ +object ErrorCodes { + val DUPLICATE_ENTRY = ErrorCode("DUPLICATE_ENTRY") + val CONSTRAINT_VIOLATION = ErrorCode("CONSTRAINT_VIOLATION") + val FOREIGN_KEY_VIOLATION = ErrorCode("FOREIGN_KEY_VIOLATION") + val CHECK_VIOLATION = ErrorCode("CHECK_VIOLATION") + val DATABASE_TIMEOUT = ErrorCode("DATABASE_TIMEOUT") + val DATABASE_ERROR = ErrorCode("DATABASE_ERROR") + val TRANSACTION_ERROR = ErrorCode("TRANSACTION_ERROR") + val VALIDATION_ERROR = ErrorCode("VALIDATION_ERROR") +} diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/ValidationError.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/ValidationError.kt index 756b5dc0..71817b08 100644 --- a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/ValidationError.kt +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/ValidationError.kt @@ -8,38 +8,38 @@ import kotlinx.serialization.Serializable */ @Serializable data class ValidationError( - val field: String, - val message: String, - val code: String + val field: String, + val message: String, + val code: String ) : BaseDto { - companion object { - /** - * Erzeugt einen Validierungsfehler für Pflichtfeld-Prüfungen. - */ - fun required(field: String): ValidationError { - return ValidationError(field, "$field ist erforderlich", "REQUIRED") - } - - /** - * Erzeugt einen Validierungsfehler für ungültiges Format. - */ - fun invalidFormat(field: String, message: String = "Ungültiges Format"): ValidationError { - return ValidationError(field, message, "INVALID_FORMAT") - } - - /** - * Erzeugt einen Validierungsfehler für Längenprüfungen. - */ - fun invalidLength(field: String, message: String): ValidationError { - return ValidationError(field, message, "INVALID_LENGTH") - } - - /** - * Erzeugt einen Validierungsfehler für Bereichsprüfungen. - */ - fun invalidRange(field: String, message: String): ValidationError { - return ValidationError(field, message, "INVALID_RANGE") - } + companion object { + /** + * Erzeugt einen Validierungsfehler für Pflichtfeld-Prüfungen. + */ + fun required(field: String): ValidationError { + return ValidationError(field, "$field ist erforderlich", "REQUIRED") } + + /** + * Erzeugt einen Validierungsfehler für ungültiges Format. + */ + fun invalidFormat(field: String, message: String = "Ungültiges Format"): ValidationError { + return ValidationError(field, message, "INVALID_FORMAT") + } + + /** + * Erzeugt einen Validierungsfehler für Längenprüfungen. + */ + fun invalidLength(field: String, message: String): ValidationError { + return ValidationError(field, message, "INVALID_LENGTH") + } + + /** + * Erzeugt einen Validierungsfehler für Bereichsprüfungen. + */ + fun invalidRange(field: String, message: String): ValidationError { + return ValidationError(field, message, "INVALID_RANGE") + } + } } diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/ValueTypes.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/ValueTypes.kt index fa8ee041..f2cfe25b 100644 --- a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/ValueTypes.kt +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/ValueTypes.kt @@ -1,4 +1,5 @@ @file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + package at.mocode.core.domain.model import at.mocode.core.domain.serialization.UuidSerializer @@ -19,7 +20,7 @@ import kotlin.uuid.Uuid @Serializable @JvmInline value class EntityId(@Serializable(with = UuidSerializer::class) val value: Uuid) { - companion object + companion object } /** @@ -28,7 +29,7 @@ value class EntityId(@Serializable(with = UuidSerializer::class) val value: Uuid @Serializable @JvmInline value class EventId(@Serializable(with = UuidSerializer::class) val value: Uuid) { - companion object + companion object } /** @@ -37,7 +38,7 @@ value class EventId(@Serializable(with = UuidSerializer::class) val value: Uuid) @Serializable @JvmInline value class AggregateId(@Serializable(with = UuidSerializer::class) val value: Uuid) { - companion object + companion object } /** @@ -46,7 +47,7 @@ value class AggregateId(@Serializable(with = UuidSerializer::class) val value: U @Serializable @JvmInline value class CorrelationId(@Serializable(with = UuidSerializer::class) val value: Uuid) { - companion object + companion object } /** @@ -55,7 +56,7 @@ value class CorrelationId(@Serializable(with = UuidSerializer::class) val value: @Serializable @JvmInline value class CausationId(@Serializable(with = UuidSerializer::class) val value: Uuid) { - companion object + companion object } // === Domain Value Classes === @@ -66,14 +67,14 @@ value class CausationId(@Serializable(with = UuidSerializer::class) val value: U @Serializable @JvmInline value class EventType(val value: String) { - init { - require(value.isNotBlank()) { "Event type cannot be blank" } - require(value.matches(Regex("^[A-Za-z][A-Za-z0-9]*$"))) { - "Event type must start with a letter and contain only alphanumeric characters" - } + init { + require(value.isNotBlank()) { "Event type cannot be blank" } + require(value.matches(Regex("^[A-Za-z][A-Za-z0-9]*$"))) { + "Event type must start with a letter and contain only alphanumeric characters" } + } - override fun toString(): String = value + override fun toString(): String = value } /** @@ -82,13 +83,13 @@ value class EventType(val value: String) { @Serializable @JvmInline value class EventVersion(val value: Long) : Comparable { - init { - require(value >= 0) { "Event version must be non-negative" } - } + init { + require(value >= 0) { "Event version must be non-negative" } + } - override fun toString(): String = value.toString() + override fun toString(): String = value.toString() - override fun compareTo(other: EventVersion): Int = value.compareTo(other.value) + override fun compareTo(other: EventVersion): Int = value.compareTo(other.value) } /** @@ -97,14 +98,14 @@ value class EventVersion(val value: Long) : Comparable { @Serializable @JvmInline value class ErrorCode(val value: String) { - init { - require(value.isNotBlank()) { "Error code cannot be blank" } - require(value.matches(Regex("^[A-Z][A-Z0-9_]*$"))) { - "Error code must be uppercase and contain only letters, numbers, and underscores" - } + init { + require(value.isNotBlank()) { "Error code cannot be blank" } + require(value.matches(Regex("^[A-Z][A-Z0-9_]*$"))) { + "Error code must be uppercase and contain only letters, numbers, and underscores" } + } - override fun toString(): String = value + override fun toString(): String = value } /** @@ -113,11 +114,11 @@ value class ErrorCode(val value: String) { @Serializable @JvmInline value class PageNumber(val value: Int) { - init { - require(value >= 0) { "Page number must be non-negative" } - } + init { + require(value >= 0) { "Page number must be non-negative" } + } - override fun toString(): String = value.toString() + override fun toString(): String = value.toString() } /** @@ -126,10 +127,10 @@ value class PageNumber(val value: Int) { @Serializable @JvmInline value class PageSize(val value: Int) { - init { - require(value > 0) { "Page size must be positive" } - require(value <= 1000) { "Page size cannot exceed 1000" } - } + init { + require(value > 0) { "Page size must be positive" } + require(value <= 1000) { "Page size cannot exceed 1000" } + } - override fun toString(): String = value.toString() + override fun toString(): String = value.toString() } diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/KotlinLocalDateSerializer.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/KotlinLocalDateSerializer.kt index fd8f92f2..d39c915d 100644 --- a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/KotlinLocalDateSerializer.kt +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/KotlinLocalDateSerializer.kt @@ -14,19 +14,19 @@ import kotlinx.serialization.encoding.Encoder * Serializes as ISO-8601 date string (yyyy-MM-dd). */ object KotlinLocalDateSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("KotlinLocalDate", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("KotlinLocalDate", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: LocalDate) { - encoder.encodeString(value.toString()) - } + override fun serialize(encoder: Encoder, value: LocalDate) { + encoder.encodeString(value.toString()) + } - override fun deserialize(decoder: Decoder): LocalDate { - val text = decoder.decodeString() - return try { - LocalDate.parse(text) - } catch (e: Exception) { - throw SerializationException("Invalid LocalDate format: '$text'", e) - } + override fun deserialize(decoder: Decoder): LocalDate { + val text = decoder.decodeString() + return try { + LocalDate.parse(text) + } catch (e: Exception) { + throw SerializationException("Invalid LocalDate format: '$text'", e) } + } } diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/KotlinxInstantSerializer.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/KotlinxInstantSerializer.kt index d21826ed..3b5f9eeb 100644 --- a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/KotlinxInstantSerializer.kt +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/KotlinxInstantSerializer.kt @@ -1,7 +1,6 @@ -@file:OptIn(kotlin.time.ExperimentalTime::class) package at.mocode.core.domain.serialization -import kotlinx.datetime.Instant +import kotlin.time.Instant import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor @@ -10,17 +9,17 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder /** - * Serializer for kotlinx.datetime.Instant. + * Serializer for kotlin.time.Instant. * Uses ISO-8601 string representation. */ object KotlinxInstantSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("KotlinxInstant", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("KotlinxInstant", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: Instant) { - encoder.encodeString(value.toString()) - } + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.toString()) + } - override fun deserialize(decoder: Decoder): Instant { - return Instant.parse(decoder.decodeString()) - } + override fun deserialize(decoder: Decoder): Instant { + return Instant.parse(decoder.decodeString()) + } } diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/Serializers.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/Serializers.kt index e591a043..01a096c7 100644 --- a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/Serializers.kt +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/Serializers.kt @@ -20,15 +20,15 @@ import kotlin.uuid.Uuid */ @OptIn(ExperimentalTime::class) object KotlinInstantSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: Instant) { - encoder.encodeString(value.toString()) - } + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.toString()) + } - override fun deserialize(decoder: Decoder): Instant { - return Instant.parse(decoder.decodeString()) - } + override fun deserialize(decoder: Decoder): Instant { + return Instant.parse(decoder.decodeString()) + } } // Note: Serializer for kotlinx.datetime.Instant is defined in a separate file @@ -39,15 +39,15 @@ object KotlinInstantSerializer : KSerializer { */ @OptIn(ExperimentalUuidApi::class) object UuidSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: Uuid) { - encoder.encodeString(value.toString()) - } + override fun serialize(encoder: Encoder, value: Uuid) { + encoder.encodeString(value.toString()) + } - override fun deserialize(decoder: Decoder): Uuid { - return Uuid.parse(decoder.decodeString()) - } + override fun deserialize(decoder: Decoder): Uuid { + return Uuid.parse(decoder.decodeString()) + } } /** @@ -55,15 +55,15 @@ object UuidSerializer : KSerializer { * Konvertiert LocalDate zu/von ISO-8601 String-Repräsentation. */ object LocalDateSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: LocalDate) { - encoder.encodeString(value.toString()) - } + override fun serialize(encoder: Encoder, value: LocalDate) { + encoder.encodeString(value.toString()) + } - override fun deserialize(decoder: Decoder): LocalDate { - return LocalDate.parse(decoder.decodeString()) - } + override fun deserialize(decoder: Decoder): LocalDate { + return LocalDate.parse(decoder.decodeString()) + } } /** @@ -71,15 +71,15 @@ object LocalDateSerializer : KSerializer { * Konvertiert LocalDateTime zu/von ISO-8601 String-Repräsentation. */ object LocalDateTimeSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: LocalDateTime) { - encoder.encodeString(value.toString()) - } + override fun serialize(encoder: Encoder, value: LocalDateTime) { + encoder.encodeString(value.toString()) + } - override fun deserialize(decoder: Decoder): LocalDateTime { - return LocalDateTime.parse(decoder.decodeString()) - } + override fun deserialize(decoder: Decoder): LocalDateTime { + return LocalDateTime.parse(decoder.decodeString()) + } } /** @@ -87,13 +87,13 @@ object LocalDateTimeSerializer : KSerializer { * Konvertiert LocalTime zu/von ISO-8601 String-Repräsentation. */ object LocalTimeSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalTime", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalTime", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: LocalTime) { - encoder.encodeString(value.toString()) - } + override fun serialize(encoder: Encoder, value: LocalTime) { + encoder.encodeString(value.toString()) + } - override fun deserialize(decoder: Decoder): LocalTime { - return LocalTime.parse(decoder.decodeString()) - } + override fun deserialize(decoder: Decoder): LocalTime { + return LocalTime.parse(decoder.decodeString()) + } } diff --git a/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/ApiResponseTest.kt b/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/ApiResponseTest.kt index 1363dddd..5216953d 100644 --- a/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/ApiResponseTest.kt +++ b/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/ApiResponseTest.kt @@ -12,42 +12,42 @@ import kotlin.test.assertTrue @OptIn(kotlin.time.ExperimentalTime::class) class ApiResponseTest { - @Test - fun `success factory sets flags and timestamp`() { - val res = ApiResponse.success(data = 42) - assertTrue(res.success) - assertEquals(42, res.data) - assertTrue(res.errors.isEmpty()) - assertNotNull(res.timestamp) - } + @Test + fun `success factory sets flags and timestamp`() { + val res = ApiResponse.success(data = 42) + assertTrue(res.success) + assertEquals(42, res.data) + assertTrue(res.errors.isEmpty()) + assertNotNull(res.timestamp) + } - @Test - fun `error factory with code object`() { - val res = ApiResponse.error(ErrorCode("INVALID_INPUT"), "Fehlerhafte Eingabe", field = "name") - assertFalse(res.success) - assertNull(res.data) - assertEquals(1, res.errors.size) - assertEquals("INVALID_INPUT", res.errors.first().code.value) - assertEquals("Fehlerhafte Eingabe", res.errors.first().message) - assertEquals("name", res.errors.first().field) - assertNotNull(res.timestamp) - } + @Test + fun `error factory with code object`() { + val res = ApiResponse.error(ErrorCode("INVALID_INPUT"), "Fehlerhafte Eingabe", field = "name") + assertFalse(res.success) + assertNull(res.data) + assertEquals(1, res.errors.size) + assertEquals("INVALID_INPUT", res.errors.first().code.value) + assertEquals("Fehlerhafte Eingabe", res.errors.first().message) + assertEquals("name", res.errors.first().field) + assertNotNull(res.timestamp) + } - @Test - fun `error factory with code string`() { - val res = ApiResponse.error("NOT_FOUND", "Nicht gefunden") - assertFalse(res.success) - assertNull(res.data) - assertEquals(1, res.errors.size) - assertEquals("NOT_FOUND", res.errors.first().code.value) - } + @Test + fun `error factory with code string`() { + val res = ApiResponse.error("NOT_FOUND", "Nicht gefunden") + assertFalse(res.success) + assertNull(res.data) + assertEquals(1, res.errors.size) + assertEquals("NOT_FOUND", res.errors.first().code.value) + } - @Test - fun `error factory with list`() { - val res = ApiResponse.error(listOf()) - assertFalse(res.success) - assertNull(res.data) - assertTrue(res.errors.isEmpty()) - assertNotNull(res.timestamp) - } + @Test + fun `error factory with list`() { + val res = ApiResponse.error(listOf()) + assertFalse(res.success) + assertNull(res.data) + assertTrue(res.errors.isEmpty()) + assertNotNull(res.timestamp) + } } diff --git a/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/BaseDomainEventTest.kt b/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/BaseDomainEventTest.kt index ca1b0523..1d51133d 100644 --- a/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/BaseDomainEventTest.kt +++ b/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/BaseDomainEventTest.kt @@ -10,54 +10,54 @@ import kotlin.uuid.Uuid @OptIn(kotlin.time.ExperimentalTime::class) class BaseDomainEventTest { - @kotlinx.serialization.Serializable - data class TestEvent( - val name: String, - // Delegiert an BaseDomainEvent - private val base: BaseDomainEvent - ) : BaseDomainEvent( - aggregateId = base.aggregateId, - eventType = base.eventType, - version = base.version, - eventId = base.eventId, - timestamp = base.timestamp, - correlationId = base.correlationId, - causationId = base.causationId - ) + @kotlinx.serialization.Serializable + data class TestEvent( + val name: String, + // Delegiert an BaseDomainEvent + private val base: BaseDomainEvent + ) : BaseDomainEvent( + aggregateId = base.aggregateId, + eventType = base.eventType, + version = base.version, + eventId = base.eventId, + timestamp = base.timestamp, + correlationId = base.correlationId, + causationId = base.causationId + ) - @Test - fun `secondary constructor generates id and timestamp`() { - val aggId = AggregateId(Uuid.random()) - val ev = object : BaseDomainEvent( - aggregateId = aggId, - eventType = EventType("TestEvent"), - version = EventVersion(1) - ) {} + @Test + fun `secondary constructor generates id and timestamp`() { + val aggId = AggregateId(Uuid.random()) + val ev = object : BaseDomainEvent( + aggregateId = aggId, + eventType = EventType("TestEvent"), + version = EventVersion(1) + ) {} - assertNotNull(ev.eventId) - assertNotNull(ev.timestamp) - assertEquals(aggId, ev.aggregateId) - assertEquals(EventType("TestEvent"), ev.eventType) - assertEquals(EventVersion(1), ev.version) - } + assertNotNull(ev.eventId) + assertNotNull(ev.timestamp) + assertEquals(aggId, ev.aggregateId) + assertEquals(EventType("TestEvent"), ev.eventType) + assertEquals(EventVersion(1), ev.version) + } - @Test - fun `primary constructor uses provided id and timestamp`() { - val aggId = AggregateId(Uuid.random()) - val eid = EventId(Uuid.random()) - val ts = kotlin.time.Instant.parse("2025-01-01T00:00:00Z") - val base = object : BaseDomainEvent( - aggregateId = aggId, - eventType = EventType("TestEvent"), - version = EventVersion(2), - eventId = eid, - timestamp = ts, - correlationId = CorrelationId(Uuid.random()), - causationId = CausationId(Uuid.random()) - ) {} + @Test + fun `primary constructor uses provided id and timestamp`() { + val aggId = AggregateId(Uuid.random()) + val eid = EventId(Uuid.random()) + val ts = kotlin.time.Instant.parse("2025-01-01T00:00:00Z") + val base = object : BaseDomainEvent( + aggregateId = aggId, + eventType = EventType("TestEvent"), + version = EventVersion(2), + eventId = eid, + timestamp = ts, + correlationId = CorrelationId(Uuid.random()), + causationId = CausationId(Uuid.random()) + ) {} - assertEquals(eid, base.eventId) - assertEquals(ts, base.timestamp) - assertEquals(EventVersion(2), base.version) - } + assertEquals(eid, base.eventId) + assertEquals(ts, base.timestamp) + assertEquals(EventVersion(2), base.version) + } } diff --git a/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/SerializersTest.kt b/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/SerializersTest.kt index fa40de62..72639986 100644 --- a/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/SerializersTest.kt +++ b/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/SerializersTest.kt @@ -12,43 +12,43 @@ import kotlin.uuid.Uuid @OptIn(kotlin.time.ExperimentalTime::class) class SerializersTest { - @Test - fun `Instant roundtrip`() { - val instant = kotlin.time.Instant.parse("2024-01-01T00:00:00Z") - val json = Json.encodeToString(KotlinInstantSerializer, instant) - val decoded = Json.decodeFromString(KotlinInstantSerializer, json) - assertEquals(instant, decoded) - } + @Test + fun `Instant roundtrip`() { + val instant = kotlin.time.Instant.parse("2024-01-01T00:00:00Z") + val json = Json.encodeToString(KotlinInstantSerializer, instant) + val decoded = Json.decodeFromString(KotlinInstantSerializer, json) + assertEquals(instant, decoded) + } - @Test - fun `UUID roundtrip`() { - val uuid = Uuid.random() - val json = Json.encodeToString(UuidSerializer, uuid) - val decoded = Json.decodeFromString(UuidSerializer, json) - assertEquals(uuid, decoded) - } + @Test + fun `UUID roundtrip`() { + val uuid = Uuid.random() + val json = Json.encodeToString(UuidSerializer, uuid) + val decoded = Json.decodeFromString(UuidSerializer, json) + assertEquals(uuid, decoded) + } - @Test - fun `LocalDate roundtrip`() { - val ld = LocalDate.parse("2024-06-15") - val json = Json.encodeToString(LocalDateSerializer, ld) - val decoded = Json.decodeFromString(LocalDateSerializer, json) - assertEquals(ld, decoded) - } + @Test + fun `LocalDate roundtrip`() { + val ld = LocalDate.parse("2024-06-15") + val json = Json.encodeToString(LocalDateSerializer, ld) + val decoded = Json.decodeFromString(LocalDateSerializer, json) + assertEquals(ld, decoded) + } - @Test - fun `LocalDateTime roundtrip`() { - val ldt = LocalDateTime.parse("2024-06-15T12:34:56") - val json = Json.encodeToString(LocalDateTimeSerializer, ldt) - val decoded = Json.decodeFromString(LocalDateTimeSerializer, json) - assertEquals(ldt, decoded) - } + @Test + fun `LocalDateTime roundtrip`() { + val ldt = LocalDateTime.parse("2024-06-15T12:34:56") + val json = Json.encodeToString(LocalDateTimeSerializer, ldt) + val decoded = Json.decodeFromString(LocalDateTimeSerializer, json) + assertEquals(ldt, decoded) + } - @Test - fun `LocalTime roundtrip`() { - val lt = LocalTime.parse("12:34:56") - val json = Json.encodeToString(LocalTimeSerializer, lt) - val decoded = Json.decodeFromString(LocalTimeSerializer, json) - assertEquals(lt, decoded) - } + @Test + fun `LocalTime roundtrip`() { + val lt = LocalTime.parse("12:34:56") + val json = Json.encodeToString(LocalTimeSerializer, lt) + val decoded = Json.decodeFromString(LocalTimeSerializer, json) + assertEquals(lt, decoded) + } } diff --git a/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/ValueTypesTest.kt b/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/ValueTypesTest.kt index 0fdc4c0b..2ae6ee9d 100644 --- a/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/ValueTypesTest.kt +++ b/core/core-domain/src/commonTest/kotlin/at/mocode/core/domain/ValueTypesTest.kt @@ -8,39 +8,39 @@ import kotlin.test.assertTrue class ValueTypesTest { - @Test - fun `EventType validation works`() { - assertFailsWith { EventType("") } - assertFailsWith { EventType("1Bad") } - assertFailsWith { EventType("bad-char!") } - assertEquals("OrderCreated", EventType("OrderCreated").toString()) - } + @Test + fun `EventType validation works`() { + assertFailsWith { EventType("") } + assertFailsWith { EventType("1Bad") } + assertFailsWith { EventType("bad-char!") } + assertEquals("OrderCreated", EventType("OrderCreated").toString()) + } - @Test - fun `EventVersion must be non-negative and comparable`() { - assertFailsWith { EventVersion(-1) } - assertEquals(0, EventVersion(0).compareTo(EventVersion(0))) - assertTrue(EventVersion(2) > EventVersion(1)) - } + @Test + fun `EventVersion must be non-negative and comparable`() { + assertFailsWith { EventVersion(-1) } + assertEquals(0, EventVersion(0).compareTo(EventVersion(0))) + assertTrue(EventVersion(2) > EventVersion(1)) + } - @Test - fun `ErrorCode must be uppercase with allowed characters`() { - assertFailsWith { ErrorCode("") } - assertFailsWith { ErrorCode("abc") } - assertFailsWith { ErrorCode("Bad_Code") } - assertEquals("VALID_CODE1", ErrorCode("VALID_CODE1").toString()) - } + @Test + fun `ErrorCode must be uppercase with allowed characters`() { + assertFailsWith { ErrorCode("") } + assertFailsWith { ErrorCode("abc") } + assertFailsWith { ErrorCode("Bad_Code") } + assertEquals("VALID_CODE1", ErrorCode("VALID_CODE1").toString()) + } - @Test - fun `PageNumber must be non-negative`() { - assertFailsWith { PageNumber(-1) } - assertEquals("0", PageNumber(0).toString()) - } + @Test + fun `PageNumber must be non-negative`() { + assertFailsWith { PageNumber(-1) } + assertEquals("0", PageNumber(0).toString()) + } - @Test - fun `PageSize range is enforced`() { - assertFailsWith { PageSize(0) } - assertFailsWith { PageSize(1001) } - assertEquals("1000", PageSize(1000).toString()) - } + @Test + fun `PageSize range is enforced`() { + assertFailsWith { PageSize(0) } + assertFailsWith { PageSize(1001) } + assertEquals("1000", PageSize(1000).toString()) + } } diff --git a/core/core-utils/build.gradle.kts b/core/core-utils/build.gradle.kts index f093dbfa..3bf47fb1 100644 --- a/core/core-utils/build.gradle.kts +++ b/core/core-utils/build.gradle.kts @@ -1,75 +1,75 @@ // Dieses Modul stellt gemeinsame technische Hilfsfunktionen bereit, // wie z.B. Konfigurations-Management, Datenbank-Verbindungen und Service Discovery. plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) } kotlin { - jvmToolchain(21) + jvmToolchain(21) - // Target platforms - jvm { - compilerOptions { - freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") - } + // Target platforms + jvm { + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + } + } + + js(IR) { + browser { + testTask { + enabled = false + } + } + } + + sourceSets { + all { + languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi") } - js(IR) { - browser { - testTask { - enabled = false - } - } + commonMain.dependencies { + // Domain models and types (core-utils depends on core-domain, not vice versa) + api(projects.core.coreDomain) + + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.datetime) + // Async support (available for all platforms) + api(libs.kotlinx.coroutines.core) + // Utilities (multiplatform compatible) + api(libs.bignum) } - sourceSets { - all { - languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi") - } - - commonMain.dependencies { - // Domain models and types (core-utils depends on core-domain, not vice versa) - api(projects.core.coreDomain) - - api(libs.kotlinx.serialization.json) - api(libs.kotlinx.datetime) - // Async support (available for all platforms) - api(libs.kotlinx.coroutines.core) - // Utilities (multiplatform compatible) - api(libs.bignum) - } - - commonTest.dependencies { - implementation(libs.kotlin.test) - } - - jvmMain.dependencies { - // JVM-specific dependencies - access to central catalog - api(projects.platform.platformDependencies) - // Database Management (JVM-specific) - api(libs.bundles.exposed) - api(libs.bundles.flyway) - api(libs.hikari.cp) - // Service Discovery (JVM-specific) - api(libs.spring.cloud.starter.consul.discovery) - // Logging (JVM-specific) - api(libs.kotlin.logging.jvm) - // Jakarta Annotation API - api(libs.jakarta.annotation.api) - // JSON Processing - api(libs.jackson.module.kotlin) - api(libs.jackson.datatype.jsr310) - } - jvmTest.dependencies { - // Testing (JVM-specific) - implementation(projects.platform.platformTesting) - implementation(libs.bundles.testing.jvm) - runtimeOnly(libs.postgresql.driver) - } + commonTest.dependencies { + implementation(libs.kotlin.test) } + + jvmMain.dependencies { + // JVM-specific dependencies - access to central catalog + api(projects.platform.platformDependencies) + // Database Management (JVM-specific) + api(libs.bundles.exposed) + api(libs.bundles.flyway) + api(libs.hikari.cp) + // Service Discovery (JVM-specific) + api(libs.spring.cloud.starter.consul.discovery) + // Logging (JVM-specific) + api(libs.kotlin.logging.jvm) + // Jakarta Annotation API + api(libs.jakarta.annotation.api) + // JSON Processing + api(libs.jackson.module.kotlin) + api(libs.jackson.datatype.jsr310) + } + jvmTest.dependencies { + // Testing (JVM-specific) + implementation(projects.platform.platformTesting) + implementation(libs.bundles.testing.jvm) + runtimeOnly(libs.postgresql.driver) + } + } } tasks.named("jvmTest") { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Extensions.kt b/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Extensions.kt index 7c9e4d79..61bb0d3c 100644 --- a/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Extensions.kt +++ b/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Extensions.kt @@ -1,4 +1,5 @@ @file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + package at.mocode.core.utils import at.mocode.core.domain.model.* @@ -54,14 +55,14 @@ fun String.toErrorCode(): ErrorCode = ErrorCode(this) * Prüft ob der String ein gültiger EventType-Name ist. */ fun String.isValidEventType(): Boolean { - return isNotBlank() && matches(Regex("^[A-Za-z][A-Za-z0-9]*$")) + return isNotBlank() && matches(Regex("^[A-Za-z][A-Za-z0-9]*$")) } /** * Prüft ob der String ein gültiger ErrorCode ist. */ fun String.isValidErrorCode(): Boolean { - return isNotBlank() && matches(Regex("^[A-Z][A-Z0-9_]*$")) + return isNotBlank() && matches(Regex("^[A-Z][A-Z0-9_]*$")) } // === Collection Extensions === @@ -70,22 +71,22 @@ fun String.isValidErrorCode(): Boolean { * Erstellt eine PagedResponse aus einer Liste mit Standard-Paginierung. */ fun List.toPagedResponse( - page: Int = 0, - size: Int = 20 + page: Int = 0, + size: Int = 20 ): PagedResponse { - val startIndex = page * size - val endIndex = minOf(startIndex + size, this.size) - val content = if (startIndex < this.size) this.subList(startIndex, endIndex) else emptyList() + val startIndex = page * size + val endIndex = minOf(startIndex + size, this.size) + val content = if (startIndex < this.size) this.subList(startIndex, endIndex) else emptyList() - return PagedResponse.create( - content = content, - page = page, - size = size, - totalElements = this.size.toLong(), - totalPages = (this.size + size - 1) / size, - hasNext = endIndex < this.size, - hasPrevious = page > 0 - ) + return PagedResponse.create( + content = content, + page = page, + size = size, + totalElements = this.size.toLong(), + totalPages = (this.size + size - 1) / size, + hasNext = endIndex < this.size, + hasPrevious = page > 0 + ) } // === Validation Extensions === @@ -94,7 +95,7 @@ fun List.toPagedResponse( * Erstellt eine Liste von ValidationError aus einer Map von Fehlern. */ fun Map.toValidationErrors(): List { - return this.map { (field, message) -> ValidationError(field, message, "VALIDATION_ERROR") } + return this.map { (field, message) -> ValidationError(field, message, "VALIDATION_ERROR") } } /** @@ -106,7 +107,7 @@ fun List.hasErrors(): Boolean = this.isNotEmpty() * Konvertiert eine Liste von ValidationError zu ErrorDto. */ fun List.toErrorDtos(): List { - return this.map { ErrorDto(ErrorCode(it.code), it.message, it.field) } + return this.map { ErrorDto(ErrorCode(it.code), it.message, it.field) } } // === Time Extensions === diff --git a/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Result.kt b/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Result.kt index 3e726214..aa839415 100644 --- a/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Result.kt +++ b/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Result.kt @@ -9,315 +9,333 @@ import kotlin.jvm.JvmName * Bietet einen funktionalen Ansatz zur Fehlerbehandlung ohne Exceptions. */ sealed class Result { - /** - * Represents a successful operation with a value. - */ - data class Success(val value: T) : Result() + /** + * Represents a successful operation with a value. + */ + data class Success(val value: T) : Result() - /** - * Represents a failed operation with error messages. - */ - data class Failure(val errors: List) : Result() + /** + * Represents a failed operation with error messages. + */ + data class Failure(val errors: List) : Result() - /** - * Checks if the Result is a success. - */ - val isSuccess: Boolean get() = this is Success + /** + * Checks if the Result is a success. + */ + val isSuccess: Boolean get() = this is Success - /** - * Checks if the Result is a failure. - */ - val isFailure: Boolean get() = this is Failure + /** + * Checks if the Result is a failure. + */ + val isFailure: Boolean get() = this is Failure + /** + * Gets the value if it's a success, otherwise null. + * + * @return the value if this is a Success, or null if this is a Failure + */ + fun getOrNull(): T? = when (this) { + is Success -> value + is Failure -> null + } + + /** + * Gets the value if it's a success, otherwise the default value. + * + * @param defaultValue the value to return if this is a Failure + * @return the value if this is a Success, or the default value if this is a Failure + */ + fun getOrDefault(defaultValue: @UnsafeVariance T): T = when (this) { + is Success -> value + is Failure -> defaultValue + } + + /** + * Gets the errors if it's a failure, otherwise an empty list. + * + * @return the list of errors if this is a Failure, or an empty list if this is a Success + */ + @JvmName("retrieveErrors") + fun getErrors(): List = when (this) { + is Success -> emptyList() + is Failure -> errors + } + + /** + * Transforms the value if it's a success. + * + * @param transform function to apply to the success value + * @return a new Success with the transformed value if this is a Success, or this unchanged Failure + */ + inline fun map(transform: (T) -> R): Result = when (this) { + is Success -> Success(transform(value)) + is Failure -> this + } + + /** + * Transforms the Result flatly (for nested Results). + * Unlike map, which wraps the transformed value in a new Success, flatMap uses the Result returned by the transform function. + * + * @param transform function that returns a Result + * @return the Result returned by the transform function if this is a Success, or this unchanged Failure + */ + inline fun flatMap(transform: (T) -> Result): Result = when (this) { + is Success -> transform(value) + is Failure -> this + } + + /** + * Executes an action if it's a success. + * + * @param action the function to execute with the success value + * @return this Result, unchanged, to allow for chaining + */ + inline fun onSuccess(action: (T) -> Unit): Result { + if (this is Success) action(value) + return this + } + + /** + * Executes an action if it's a failure. + * + * @param action the function to execute with the list of errors + * @return this Result, unchanged, to allow for chaining + */ + inline fun onFailure(action: (List) -> Unit): Result { + if (this is Failure) action(errors) + return this + } + + /** + * Transforms the Result by applying one of two functions depending on whether it's a success or failure. + * + * @param onSuccess function to apply if this is a success + * @param onFailure function to apply if this is a failure + * @return the result of applying the appropriate function + */ + inline fun fold( + onSuccess: (T) -> R, + onFailure: (List) -> R + ): R = when (this) { + is Success -> onSuccess(value) + is Failure -> onFailure(errors) + } + + /** + * Attempts to recover from a failure by applying the specified function to the error list. + * If this is already a success, it is returned unchanged. + * + * @param transform function to apply to the error list to recover + * @return a new Success if recovery was successful, or this unchanged Result if already a success + */ + inline fun recover(transform: (List) -> @UnsafeVariance T): Result = when (this) { + is Success -> this + is Failure -> Success(transform(errors)) + } + + /** + * Attempts to recover from a failure by applying the specified function to the error list. + * If this is already a success, it is returned unchanged. + * If an exception occurs during recovery, it is converted to a new failure. + * + * @param transform function to apply to the error list to recover + * @return a new Success if recovery was successful, a new Failure if recovery threw an exception, + * or this unchanged Result if already a success + */ + inline fun recoverCatching(transform: (List) -> @UnsafeVariance T): Result = when (this) { + is Success -> this + is Failure -> try { + Success(transform(errors)) + } catch (e: Exception) { + Failure( + listOf( + ErrorDto( + code = at.mocode.core.domain.model.ErrorCode("RECOVERY_FAILED"), + message = e.message ?: "Recovery failed with an unknown error" + ) + ) + ) + } + } + + /** + * Combines this Result with another Result, creating a pair of their values if both are successful. + * If either Result is a failure, the combined Result will be a failure containing all errors. + * + * @param other the Result to combine with this one + * @return a Result containing a Pair of values if both are successful, or a Failure with all errors + */ + fun zip(other: Result): Result> = when { + this is Success && other is Success -> Success(Pair(this.value, other.value)) + this is Success && other is Failure -> Failure(other.errors) + this is Failure && other is Success -> Failure(this.errors) + this is Failure && other is Failure -> { + val allErrors = this.errors + other.errors + Failure(allErrors) + } + // This branch should never be reached due to sealed class, but included for completeness + else -> throw IllegalStateException("Unreachable code - Result should be either Success or Failure") + } + + /** + * Safely attempts to get the value, throwing a custom exception if this is a failure. + * + * @param errorHandler function that converts the list of errors to an exception + * @return the value if this is a Success + * @throws E if this is a Failure, as created by the errorHandler + */ + inline fun getOrThrow(errorHandler: (List) -> E): T = when (this) { + is Success -> value + is Failure -> throw errorHandler(errors) + } + + /** + * Gets the value if it's a success, or throws an IllegalStateException with a message constructed from the errors. + * + * @return the value if this is a Success + * @throws IllegalStateException if this is a Failure, with a message containing the error details + */ + fun getOrThrow(): T = getOrThrow { errors -> + IllegalStateException("Result is a Failure with errors: ${errors.joinToString { it.message }}") + } + + companion object { /** - * Gets the value if it's a success, otherwise null. + * Creates a successful Result. * - * @return the value if this is a Success, or null if this is a Failure + * @param value the value to wrap in a Success + * @return a new Success containing the provided value */ - fun getOrNull(): T? = when (this) { - is Success -> value - is Failure -> null + fun success(value: T): Result = Success(value) + + /** + * Creates a failure Result with a single error. + * + * @param error the error to include in the Failure + * @return a new Failure containing the provided error + */ + fun failure(error: ErrorDto): Result = Failure(listOf(error)) + + /** + * Creates a failure Result with multiple errors. + * + * @param errors the list of errors to include in the Failure + * @return a new Failure containing the provided errors + */ + fun failure(errors: List): Result = Failure(errors) + + /** + * Creates a failure Result from ValidationErrors. + * Converts the ValidationErrors to ErrorDtos internally. + * + * @param validationErrors the list of validation errors to convert and include in the Failure + * @return a new Failure containing ErrorDtos converted from the provided ValidationErrors + */ + @JvmName("failureFromValidationErrors") + fun failure(validationErrors: List): Result = + Failure(validationErrors.toErrorDtos()) + + /** + * Executes an operation that returns a Result and catches exceptions. + * Provides more specific error codes based on the type of exception caught. + * + * @param operation the operation to execute + * @return a Success with the operation result, or a Failure with error details if an exception occurred + */ + inline fun runCatching(operation: () -> T): Result = try { + success(operation()) + } catch (e: IllegalArgumentException) { + failure( + ErrorDto( + code = at.mocode.core.domain.model.ErrorCode("INVALID_ARGUMENT"), + message = e.message ?: "Invalid argument provided" + ) + ) + } catch (e: IllegalStateException) { + failure( + ErrorDto( + code = at.mocode.core.domain.model.ErrorCode("INVALID_STATE"), + message = e.message ?: "Operation called in invalid state" + ) + ) + } catch (e: UnsupportedOperationException) { + failure( + ErrorDto( + code = at.mocode.core.domain.model.ErrorCode("UNSUPPORTED_OPERATION"), + message = e.message ?: "Operation not supported" + ) + ) + } catch (e: IndexOutOfBoundsException) { + failure( + ErrorDto( + code = at.mocode.core.domain.model.ErrorCode("INDEX_OUT_OF_BOUNDS"), + message = e.message ?: "Index out of bounds" + ) + ) + } catch (e: NullPointerException) { + failure( + ErrorDto( + code = at.mocode.core.domain.model.ErrorCode("NULL_REFERENCE"), + message = e.message ?: "Unexpected null reference" + ) + ) + } catch (e: ClassCastException) { + failure( + ErrorDto( + code = at.mocode.core.domain.model.ErrorCode("TYPE_MISMATCH"), + message = e.message ?: "Type mismatch occurred" + ) + ) + } catch (e: Exception) { + // Fallback for any other exception type + failure( + ErrorDto( + code = at.mocode.core.domain.model.ErrorCode("OPERATION_FAILED"), + message = e.message ?: "Unknown error occurred" + ) + ) } /** - * Gets the value if it's a success, otherwise the default value. + * Combines multiple Results into a single Result with a list. + * Optimized for performance with large collections. * - * @param defaultValue the value to return if this is a Failure - * @return the value if this is a Success, or the default value if this is a Failure + * @param results a list of Results to combine + * @return a Success containing a list of all success values if all Results are successful, + * or a Failure containing all error messages if any Results are failures */ - fun getOrDefault(defaultValue: @UnsafeVariance T): T = when (this) { - is Success -> value - is Failure -> defaultValue - } - - /** - * Gets the errors if it's a failure, otherwise an empty list. - * - * @return the list of errors if this is a Failure, or an empty list if this is a Success - */ - @JvmName("retrieveErrors") - fun getErrors(): List = when (this) { - is Success -> emptyList() - is Failure -> errors - } - - /** - * Transforms the value if it's a success. - * - * @param transform function to apply to the success value - * @return a new Success with the transformed value if this is a Success, or this unchanged Failure - */ - inline fun map(transform: (T) -> R): Result = when (this) { - is Success -> Success(transform(value)) - is Failure -> this - } - - /** - * Transforms the Result flatly (for nested Results). - * Unlike map, which wraps the transformed value in a new Success, flatMap uses the Result returned by the transform function. - * - * @param transform function that returns a Result - * @return the Result returned by the transform function if this is a Success, or this unchanged Failure - */ - inline fun flatMap(transform: (T) -> Result): Result = when (this) { - is Success -> transform(value) - is Failure -> this - } - - /** - * Executes an action if it's a success. - * - * @param action the function to execute with the success value - * @return this Result, unchanged, to allow for chaining - */ - inline fun onSuccess(action: (T) -> Unit): Result { - if (this is Success) action(value) - return this - } - - /** - * Executes an action if it's a failure. - * - * @param action the function to execute with the list of errors - * @return this Result, unchanged, to allow for chaining - */ - inline fun onFailure(action: (List) -> Unit): Result { - if (this is Failure) action(errors) - return this - } - - /** - * Transforms the Result by applying one of two functions depending on whether it's a success or failure. - * - * @param onSuccess function to apply if this is a success - * @param onFailure function to apply if this is a failure - * @return the result of applying the appropriate function - */ - inline fun fold( - onSuccess: (T) -> R, - onFailure: (List) -> R - ): R = when (this) { - is Success -> onSuccess(value) - is Failure -> onFailure(errors) - } - - /** - * Attempts to recover from a failure by applying the specified function to the error list. - * If this is already a success, it is returned unchanged. - * - * @param transform function to apply to the error list to recover - * @return a new Success if recovery was successful, or this unchanged Result if already a success - */ - inline fun recover(transform: (List) -> @UnsafeVariance T): Result = when (this) { - is Success -> this - is Failure -> Success(transform(errors)) - } - - /** - * Attempts to recover from a failure by applying the specified function to the error list. - * If this is already a success, it is returned unchanged. - * If an exception occurs during recovery, it is converted to a new failure. - * - * @param transform function to apply to the error list to recover - * @return a new Success if recovery was successful, a new Failure if recovery threw an exception, - * or this unchanged Result if already a success - */ - inline fun recoverCatching(transform: (List) -> @UnsafeVariance T): Result = when (this) { - is Success -> this - is Failure -> try { - Success(transform(errors)) - } catch (e: Exception) { - Failure(listOf(ErrorDto( - code = at.mocode.core.domain.model.ErrorCode("RECOVERY_FAILED"), - message = e.message ?: "Recovery failed with an unknown error" - ))) - } - } - - /** - * Combines this Result with another Result, creating a pair of their values if both are successful. - * If either Result is a failure, the combined Result will be a failure containing all errors. - * - * @param other the Result to combine with this one - * @return a Result containing a Pair of values if both are successful, or a Failure with all errors - */ - fun zip(other: Result): Result> = when { - this is Success && other is Success -> Success(Pair(this.value, other.value)) - this is Success && other is Failure -> Failure(other.errors) - this is Failure && other is Success -> Failure(this.errors) - this is Failure && other is Failure -> { - val allErrors = this.errors + other.errors - Failure(allErrors) - } - // This branch should never be reached due to sealed class, but included for completeness - else -> throw IllegalStateException("Unreachable code - Result should be either Success or Failure") - } - - /** - * Safely attempts to get the value, throwing a custom exception if this is a failure. - * - * @param errorHandler function that converts the list of errors to an exception - * @return the value if this is a Success - * @throws E if this is a Failure, as created by the errorHandler - */ - inline fun getOrThrow(errorHandler: (List) -> E): T = when (this) { - is Success -> value - is Failure -> throw errorHandler(errors) - } - - /** - * Gets the value if it's a success, or throws an IllegalStateException with a message constructed from the errors. - * - * @return the value if this is a Success - * @throws IllegalStateException if this is a Failure, with a message containing the error details - */ - fun getOrThrow(): T = getOrThrow { errors -> - IllegalStateException("Result is a Failure with errors: ${errors.joinToString { it.message }}") - } - - companion object { - /** - * Creates a successful Result. - * - * @param value the value to wrap in a Success - * @return a new Success containing the provided value - */ - fun success(value: T): Result = Success(value) - - /** - * Creates a failure Result with a single error. - * - * @param error the error to include in the Failure - * @return a new Failure containing the provided error - */ - fun failure(error: ErrorDto): Result = Failure(listOf(error)) - - /** - * Creates a failure Result with multiple errors. - * - * @param errors the list of errors to include in the Failure - * @return a new Failure containing the provided errors - */ - fun failure(errors: List): Result = Failure(errors) - - /** - * Creates a failure Result from ValidationErrors. - * Converts the ValidationErrors to ErrorDtos internally. - * - * @param validationErrors the list of validation errors to convert and include in the Failure - * @return a new Failure containing ErrorDtos converted from the provided ValidationErrors - */ - @JvmName("failureFromValidationErrors") - fun failure(validationErrors: List): Result = - Failure(validationErrors.toErrorDtos()) - - /** - * Executes an operation that returns a Result and catches exceptions. - * Provides more specific error codes based on the type of exception caught. - * - * @param operation the operation to execute - * @return a Success with the operation result, or a Failure with error details if an exception occurred - */ - inline fun runCatching(operation: () -> T): Result = try { - success(operation()) - } catch (e: IllegalArgumentException) { - failure(ErrorDto( - code = at.mocode.core.domain.model.ErrorCode("INVALID_ARGUMENT"), - message = e.message ?: "Invalid argument provided" - )) - } catch (e: IllegalStateException) { - failure(ErrorDto( - code = at.mocode.core.domain.model.ErrorCode("INVALID_STATE"), - message = e.message ?: "Operation called in invalid state" - )) - } catch (e: UnsupportedOperationException) { - failure(ErrorDto( - code = at.mocode.core.domain.model.ErrorCode("UNSUPPORTED_OPERATION"), - message = e.message ?: "Operation not supported" - )) - } catch (e: IndexOutOfBoundsException) { - failure(ErrorDto( - code = at.mocode.core.domain.model.ErrorCode("INDEX_OUT_OF_BOUNDS"), - message = e.message ?: "Index out of bounds" - )) - } catch (e: NullPointerException) { - failure(ErrorDto( - code = at.mocode.core.domain.model.ErrorCode("NULL_REFERENCE"), - message = e.message ?: "Unexpected null reference" - )) - } catch (e: ClassCastException) { - failure(ErrorDto( - code = at.mocode.core.domain.model.ErrorCode("TYPE_MISMATCH"), - message = e.message ?: "Type mismatch occurred" - )) - } catch (e: Exception) { - // Fallback for any other exception type - failure(ErrorDto( - code = at.mocode.core.domain.model.ErrorCode("OPERATION_FAILED"), - message = e.message ?: "Unknown error occurred" - )) - } - - /** - * Combines multiple Results into a single Result with a list. - * Optimized for performance with large collections. - * - * @param results a list of Results to combine - * @return a Success containing a list of all success values if all Results are successful, - * or a Failure containing all error messages if any Results are failures - */ - fun combine(results: List>): Result> { - // Fast path for empty list - if (results.isEmpty()) { - return success(emptyList()) - } - - // Fast path for single result - if (results.size == 1) { - return results.first().map { listOf(it) } - } - - // Check if there are any failures - val anyFailure = results.any { it.isFailure } - - // If no failures, we can optimize by directly mapping to values - if (!anyFailure) { - return success(results.map { (it as Success).value }) - } - - // If there are failures, collect all errors - val errors = results - .filterIsInstance() - .flatMap { it.errors } - - // If empty results list contained no failures, return empty success - if (errors.isEmpty()) { - return success(emptyList()) - } - - return failure(errors) - } + fun combine(results: List>): Result> { + // Fast path for empty list + if (results.isEmpty()) { + return success(emptyList()) + } + + // Fast path for single result + if (results.size == 1) { + return results.first().map { listOf(it) } + } + + // Check if there are any failures + val anyFailure = results.any { it.isFailure } + + // If no failures, we can optimize by directly mapping to values + if (!anyFailure) { + return success(results.map { (it as Success).value }) + } + + // If there are failures, collect all errors + val errors = results + .filterIsInstance() + .flatMap { it.errors } + + // If empty results list contained no failures, return empty success + if (errors.isEmpty()) { + return success(emptyList()) + } + + return failure(errors) } + } } /** @@ -328,11 +346,13 @@ sealed class Result { * @return a Success containing the non-null value, or a Failure if the value is null */ fun T?.toResult(errorMessage: String = "Value is null"): Result = - if (this != null) { - Result.success(this) - } else { - Result.failure(ErrorDto( - code = at.mocode.core.domain.model.ErrorCode("NULL_VALUE"), - message = errorMessage - )) - } + if (this != null) { + Result.success(this) + } else { + Result.failure( + ErrorDto( + code = at.mocode.core.domain.model.ErrorCode("NULL_VALUE"), + message = errorMessage + ) + ) + } diff --git a/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Validation.kt b/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Validation.kt index b92eeb74..5de5fe58 100644 --- a/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Validation.kt +++ b/core/core-utils/src/commonMain/kotlin/at/mocode/core/utils/Validation.kt @@ -11,63 +11,63 @@ import at.mocode.core.domain.model.ValidationError * Builder-Klasse für die Erstellung von Validierungsregeln. */ class ValidationBuilder { - private val errors = mutableListOf() + private val errors = mutableListOf() - /** - * Validiert ein Feld gegen mehrere Regeln. - */ - fun field(name: String, value: T, vararg rules: ValidationRule): ValidationBuilder { - rules.forEach { rule -> - rule.validate(name, value)?.let { error -> - errors.add(error) - } - } - return this + /** + * Validiert ein Feld gegen mehrere Regeln. + */ + fun field(name: String, value: T, vararg rules: ValidationRule): ValidationBuilder { + rules.forEach { rule -> + rule.validate(name, value)?.let { error -> + errors.add(error) + } } + return this + } - /** - * Fügt einen benutzerdefinierten Validierungsfehler hinzu. - */ - fun addError(field: String, message: String, code: String = "VALIDATION_ERROR"): ValidationBuilder { - errors.add(ValidationError(field, message, code)) - return this + /** + * Fügt einen benutzerdefinierten Validierungsfehler hinzu. + */ + fun addError(field: String, message: String, code: String = "VALIDATION_ERROR"): ValidationBuilder { + errors.add(ValidationError(field, message, code)) + return this + } + + /** + * Führt eine benutzerdefinierten Validierung aus. + */ + fun custom(validation: () -> ValidationError?): ValidationBuilder { + validation()?.let { error -> + errors.add(error) } + return this + } - /** - * Führt eine benutzerdefinierten Validierung aus. - */ - fun custom(validation: () -> ValidationError?): ValidationBuilder { - validation()?.let { error -> - errors.add(error) - } - return this + /** + * Erstellt das finale Validierungsergebnis. + */ + fun build(): Result { + return if (errors.isEmpty()) { + Result.success(Unit) + } else { + Result.failure(errors) } + } - /** - * Erstellt das finale Validierungsergebnis. - */ - fun build(): Result { - return if (errors.isEmpty()) { - Result.success(Unit) - } else { - Result.failure(errors) - } - } - - /** - * Gibt die gesammelten Fehler zurück. - */ - fun getErrors(): List = errors.toList() + /** + * Gibt die gesammelten Fehler zurück. + */ + fun getErrors(): List = errors.toList() } /** * Interface für Validierungsregeln. */ fun interface ValidationRule { - /** - * Validiert einen Wert und gibt einen Fehler zurück, wenn die Validierung fehlschlägt. - */ - fun validate(fieldName: String, value: T): ValidationError? + /** + * Validiert einen Wert und gibt einen Fehler zurück, wenn die Validierung fehlschlägt. + */ + fun validate(fieldName: String, value: T): ValidationError? } /** @@ -75,150 +75,150 @@ fun interface ValidationRule { */ object ValidationRules { - // === String-Validierungen === + // === String-Validierungen === - /** - * Prüft ob ein String nicht leer ist. - */ - fun notBlank(): ValidationRule = ValidationRule { fieldName, value -> - if (value.isBlank()) ValidationError.required(fieldName) else null - } + /** + * Prüft ob ein String nicht leer ist. + */ + fun notBlank(): ValidationRule = ValidationRule { fieldName, value -> + if (value.isBlank()) ValidationError.required(fieldName) else null + } - /** - * Prüft die Mindestlänge eines Strings. - */ - fun minLength(min: Int): ValidationRule = ValidationRule { fieldName, value -> - if (value.length < min) { - ValidationError.invalidLength(fieldName, "$fieldName must be at least $min characters long") - } else null - } + /** + * Prüft die Mindestlänge eines Strings. + */ + fun minLength(min: Int): ValidationRule = ValidationRule { fieldName, value -> + if (value.length < min) { + ValidationError.invalidLength(fieldName, "$fieldName muss mindestens $min Zeichen lang sein") + } else null + } - /** - * Prüft die Maximallänge eines Strings. - */ - fun maxLength(max: Int): ValidationRule = ValidationRule { fieldName, value -> - if (value.length > max) { - ValidationError.invalidLength(fieldName, "$fieldName must not exceed $max characters") - } else null - } + /** + * Prüft die Maximallänge eines Strings. + */ + fun maxLength(max: Int): ValidationRule = ValidationRule { fieldName, value -> + if (value.length > max) { + ValidationError.invalidLength(fieldName, "$fieldName darf $max Zeichen nicht überschreiten") + } else null + } - /** - * Prüft ob ein String einem RegEx-Pattern entspricht. - */ - fun matches(pattern: Regex, message: String): ValidationRule = ValidationRule { fieldName, value -> - if (!value.matches(pattern)) { - ValidationError.invalidFormat(fieldName, message) - } else null - } + /** + * Prüft ob ein String einem RegEx-Pattern entspricht. + */ + fun matches(pattern: Regex, message: String): ValidationRule = ValidationRule { fieldName, value -> + if (!value.matches(pattern)) { + ValidationError.invalidFormat(fieldName, message) + } else null + } - /** - * Prüft ob ein String eine gültige E-Mail-Adresse ist. - */ - fun email(): ValidationRule = ValidationRule { fieldName, value -> - val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$") - if (!value.matches(emailRegex)) { - ValidationError.invalidFormat(fieldName, "$fieldName must be a valid email address") - } else null - } + /** + * Prüft ob ein String eine gültige E-Mail-Adresse ist. + */ + fun email(): ValidationRule = ValidationRule { fieldName, value -> + val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$") + if (!value.matches(emailRegex)) { + ValidationError.invalidFormat(fieldName, "$fieldName muss eine gültige E-Mail-Adresse sein") + } else null + } - // === Numerische Validierungen === + // === Numerische Validierungen === - /** - * Prüft den Mindestwert einer Zahl. - */ - fun > min(minValue: T): ValidationRule = ValidationRule { fieldName, value -> - if (value < minValue) { - ValidationError.invalidRange(fieldName, "$fieldName must be at least $minValue") - } else null - } + /** + * Prüft den Mindestwert einer Zahl. + */ + fun > min(minValue: T): ValidationRule = ValidationRule { fieldName, value -> + if (value < minValue) { + ValidationError.invalidRange(fieldName, "$fieldName muss mindestens $minValue sein") + } else null + } - /** - * Prüft den Maximalwert einer Zahl. - */ - fun > max(maxValue: T): ValidationRule = ValidationRule { fieldName, value -> - if (value > maxValue) { - ValidationError.invalidRange(fieldName, "$fieldName must not exceed $maxValue") - } else null - } + /** + * Prüft den Maximalwert einer Zahl. + */ + fun > max(maxValue: T): ValidationRule = ValidationRule { fieldName, value -> + if (value > maxValue) { + ValidationError.invalidRange(fieldName, "$fieldName darf $maxValue nicht überschreiten") + } else null + } - /** - * Prüft ob eine Zahl positiv ist. - */ - fun positive(): ValidationRule = ValidationRule { fieldName, value -> - if (value.toDouble() <= 0) { - ValidationError.invalidRange(fieldName, "$fieldName must be positive") - } else null - } + /** + * Prüft ob eine Zahl positiv ist. + */ + fun positive(): ValidationRule = ValidationRule { fieldName, value -> + if (value.toDouble() <= 0) { + ValidationError.invalidRange(fieldName, "$fieldName muss positiv sein") + } else null + } - /** - * Prüft ob eine Zahl nicht negativ ist. - */ - fun nonNegative(): ValidationRule = ValidationRule { fieldName, value -> - if (value.toDouble() < 0) { - ValidationError.invalidRange(fieldName, "$fieldName must not be negative") - } else null - } + /** + * Prüft ob eine Zahl nicht negativ ist. + */ + fun nonNegative(): ValidationRule = ValidationRule { fieldName, value -> + if (value.toDouble() < 0) { + ValidationError.invalidRange(fieldName, "$fieldName darf nicht negativ sein") + } else null + } - // === Collection-Validierungen === + // === Collection-Validierungen === - /** - * Prüft ob eine Collection nicht leer ist. - */ - fun notEmpty(): ValidationRule> = ValidationRule { fieldName, value -> - if (value.isEmpty()) { - ValidationError.required(fieldName) - } else null - } + /** + * Prüft ob eine Collection nicht leer ist. + */ + fun notEmpty(): ValidationRule> = ValidationRule { fieldName, value -> + if (value.isEmpty()) { + ValidationError.required(fieldName) + } else null + } - /** - * Prüft die Mindestgröße einer Collection. - */ - fun minSize(min: Int): ValidationRule> = ValidationRule { fieldName, value -> - if (value.size < min) { - ValidationError.invalidLength(fieldName, "$fieldName must contain at least $min items") - } else null - } + /** + * Prüft die Mindestgröße einer Collection. + */ + fun minSize(min: Int): ValidationRule> = ValidationRule { fieldName, value -> + if (value.size < min) { + ValidationError.invalidLength(fieldName, "$fieldName muss mindestens $min Elemente enthalten") + } else null + } - /** - * Prüft die Maximalgröße einer Collection. - */ - fun maxSize(max: Int): ValidationRule> = ValidationRule { fieldName, value -> - if (value.size > max) { - ValidationError.invalidLength(fieldName, "$fieldName must not contain more than $max items") - } else null - } + /** + * Prüft die Maximalgröße einer Collection. + */ + fun maxSize(max: Int): ValidationRule> = ValidationRule { fieldName, value -> + if (value.size > max) { + ValidationError.invalidLength(fieldName, "$fieldName darf nicht mehr als $max Elemente enthalten") + } else null + } - // === Null-Validierungen === + // === Null-Validierungen === - /** - * Prüft ob ein Wert nicht null ist. - */ - fun notNull(): ValidationRule = ValidationRule { fieldName, value -> - if (value == null) ValidationError.required(fieldName) else null - } + /** + * Prüft ob ein Wert nicht null ist. + */ + fun notNull(): ValidationRule = ValidationRule { fieldName, value -> + if (value == null) ValidationError.required(fieldName) else null + } } /** * DSL-Funktion für die Erstellung von Validierungen. */ inline fun validate(builder: ValidationBuilder.() -> Unit): Result { - return ValidationBuilder().apply(builder).build() + return ValidationBuilder().apply(builder).build() } /** * Extension-Funktion für einfache String-Validierung. */ fun String?.validateNotBlank(fieldName: String): ValidationError? { - return if (this.isNullOrBlank()) ValidationError.required(fieldName) else null + return if (this.isNullOrBlank()) ValidationError.required(fieldName) else null } /** * Extension-Funktion für einfache E-Mail-Validierung. */ fun String?.validateEmail(fieldName: String): ValidationError? { - if (this.isNullOrBlank()) return ValidationError.required(fieldName) - val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$") - return if (!this.matches(emailRegex)) { - ValidationError.invalidFormat(fieldName, "$fieldName must be a valid email address") - } else null + if (this.isNullOrBlank()) return ValidationError.required(fieldName) + val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$") + return if (!this.matches(emailRegex)) { + ValidationError.invalidFormat(fieldName, "$fieldName muss eine gültige E-Mail-Adresse sein") + } else null } diff --git a/core/core-utils/src/commonTest/kotlin/at/mocode/core/utils/ExtensionsPagedResponseTest.kt b/core/core-utils/src/commonTest/kotlin/at/mocode/core/utils/ExtensionsPagedResponseTest.kt index e18e1b0f..f35e2f39 100644 --- a/core/core-utils/src/commonTest/kotlin/at/mocode/core/utils/ExtensionsPagedResponseTest.kt +++ b/core/core-utils/src/commonTest/kotlin/at/mocode/core/utils/ExtensionsPagedResponseTest.kt @@ -9,28 +9,28 @@ import kotlin.test.assertTrue class ExtensionsPagedResponseTest { - @Test - fun `toPagedResponse basic pagination`() { - val list = (1..50).toList() + @Test + fun `toPagedResponse basic pagination`() { + val list = (1..50).toList() - val page0 = list.toPagedResponse(page = 0, size = 10) - assertEquals(10, page0.content.size) - assertEquals(PageNumber(0), page0.page) - assertEquals(PageSize(10), page0.size) - assertEquals(50L, page0.totalElements) - assertEquals(5, page0.totalPages) - assertTrue(page0.hasNext) - assertFalse(page0.hasPrevious) + val page0 = list.toPagedResponse(page = 0, size = 10) + assertEquals(10, page0.content.size) + assertEquals(PageNumber(0), page0.page) + assertEquals(PageSize(10), page0.size) + assertEquals(50L, page0.totalElements) + assertEquals(5, page0.totalPages) + assertTrue(page0.hasNext) + assertFalse(page0.hasPrevious) - val page4 = list.toPagedResponse(page = 4, size = 10) - assertEquals((41..50).toList(), page4.content) - assertFalse(page4.hasNext) - assertTrue(page4.hasPrevious) + val page4 = list.toPagedResponse(page = 4, size = 10) + assertEquals((41..50).toList(), page4.content) + assertFalse(page4.hasNext) + assertTrue(page4.hasPrevious) - val emptyPage = list.toPagedResponse(page = 6, size = 10) - assertTrue(emptyPage.content.isEmpty()) - assertEquals(5, emptyPage.totalPages) - assertFalse(emptyPage.hasNext) - assertTrue(emptyPage.hasPrevious) - } + val emptyPage = list.toPagedResponse(page = 6, size = 10) + assertTrue(emptyPage.content.isEmpty()) + assertEquals(5, emptyPage.totalPages) + assertFalse(emptyPage.hasNext) + assertTrue(emptyPage.hasPrevious) + } } diff --git a/core/core-utils/src/commonTest/kotlin/at/mocode/core/utils/ResultTest.kt b/core/core-utils/src/commonTest/kotlin/at/mocode/core/utils/ResultTest.kt index d0d42ffe..5c019e3d 100644 --- a/core/core-utils/src/commonTest/kotlin/at/mocode/core/utils/ResultTest.kt +++ b/core/core-utils/src/commonTest/kotlin/at/mocode/core/utils/ResultTest.kt @@ -7,101 +7,103 @@ import kotlin.test.* class ResultTest { - @Test - fun `success and failure flags`() { - val s = Result.success(1) - assertTrue(s.isSuccess) - assertFalse(s.isFailure) + @Test + fun `success and failure flags`() { + val s = Result.success(1) + assertTrue(s.isSuccess) + assertFalse(s.isFailure) - val f: Result = Result.failure(ErrorDto(ErrorCode("E"), "m")) - assertTrue(f.isFailure) - assertFalse(f.isSuccess) + val f: Result = Result.failure(ErrorDto(ErrorCode("E"), "m")) + assertTrue(f.isFailure) + assertFalse(f.isSuccess) + } + + @Test + fun `map flatMap fold`() { + val s = Result.success(2).map { it * 2 } + assertEquals(4, (s as Result.Success).value) + + val f: Result = Result.failure(ErrorDto(ErrorCode("E"), "m")) + assertTrue(f.map { it + 1 } is Result.Failure) + + val flat = Result.success(2).flatMap { Result.success(it.toString()) } + assertEquals("2", (flat as Result.Success).value) + + val folded = flat.fold({ it.length }, { -1 }) + assertEquals(1, folded) + } + + @Test + fun `zip and combine`() { + val a = Result.success(1) + val b = Result.success("x") + val zipped = a.zip(b) + assertTrue(zipped is Result.Success) + assertEquals(Pair(1, "x"), (zipped as Result.Success).value) + + val f1: Result = Result.failure(ErrorDto(ErrorCode("E1"), "")) + val f2: Result = Result.failure(ErrorDto(ErrorCode("E2"), "")) + val z2 = f1.zip(b) + assertTrue(z2 is Result.Failure) + + val combined = Result.combine(listOf(Result.success(1), Result.success(2))) + assertTrue(combined is Result.Success) + assertEquals(listOf(1, 2), (combined as Result.Success).value) + + val combinedFail = + Result.combine(listOf(f1 as Result, Result.success(3), Result.failure(ErrorDto(ErrorCode("E3"), "")))) + assertTrue(combinedFail is Result.Failure) + assertEquals(2, (combinedFail as Result.Failure).errors.size) + } + + @Test + fun `runCatching failure conversion failureFromValidation and recovery`() { + val ok = Result.runCatching { "ok" } + assertTrue(ok is Result.Success) + + val iae = Result.runCatching { throw IllegalArgumentException("bad") } + assertTrue(iae is Result.Failure) + assertEquals("INVALID_ARGUMENT", (iae as Result.Failure).errors.first().code.value) + + val generic = Result.runCatching { throw Exception("x") } + assertTrue(generic is Result.Failure) + + val verrs = listOf(ValidationError.required("name"), ValidationError.invalidFormat("email")) + val fromVal: Result = Result.failure(verrs) + assertTrue(fromVal is Result.Failure) + assertEquals("REQUIRED", (fromVal as Result.Failure).errors.first().code.value) + + val rec = Result.failure(ErrorDto(ErrorCode("E"), "")).recover { _ -> "fallback" } + assertTrue(rec is Result.Success) + + val recFail = + Result.failure(ErrorDto(ErrorCode("E"), "")).recoverCatching { _ -> throw IllegalStateException("boom") } + assertTrue(recFail is Result.Failure) + assertEquals("RECOVERY_FAILED", (recFail as Result.Failure).errors.first().code.value) + } + + @Test + fun `getOrNull default throw and toResult`() { + val s = Result.success(5) + assertEquals(5, s.getOrNull()) + + val f: Result = Result.failure(ErrorDto(ErrorCode("E"), "")) + assertNull(f.getOrNull()) + assertEquals(7, f.getOrDefault(7)) + + assertEquals(5, s.getOrThrow()) + try { + f.getOrThrow() + fail("should throw") + } catch (e: IllegalStateException) { + // ok } - @Test - fun `map flatMap fold`() { - val s = Result.success(2).map { it * 2 } - assertEquals(4, (s as Result.Success).value) + val nullable: Int? = null + val r = nullable.toResult("ist leer") + assertTrue(r is Result.Failure) - val f: Result = Result.failure(ErrorDto(ErrorCode("E"), "m")) - assertTrue(f.map { it + 1 } is Result.Failure) - - val flat = Result.success(2).flatMap { Result.success(it.toString()) } - assertEquals("2", (flat as Result.Success).value) - - val folded = flat.fold({ it.length }, { -1 }) - assertEquals(1, folded) - } - - @Test - fun `zip and combine`() { - val a = Result.success(1) - val b = Result.success("x") - val zipped = a.zip(b) - assertTrue(zipped is Result.Success) - assertEquals(Pair(1, "x"), (zipped as Result.Success).value) - - val f1: Result = Result.failure(ErrorDto(ErrorCode("E1"), "")) - val f2: Result = Result.failure(ErrorDto(ErrorCode("E2"), "")) - val z2 = f1.zip(b) - assertTrue(z2 is Result.Failure) - - val combined = Result.combine(listOf(Result.success(1), Result.success(2))) - assertTrue(combined is Result.Success) - assertEquals(listOf(1, 2), (combined as Result.Success).value) - - val combinedFail = Result.combine(listOf(f1 as Result, Result.success(3), Result.failure(ErrorDto(ErrorCode("E3"), "")))) - assertTrue(combinedFail is Result.Failure) - assertEquals(2, (combinedFail as Result.Failure).errors.size) - } - - @Test - fun `runCatching failure conversion failureFromValidation and recovery`() { - val ok = Result.runCatching { "ok" } - assertTrue(ok is Result.Success) - - val iae = Result.runCatching { throw IllegalArgumentException("bad") } - assertTrue(iae is Result.Failure) - assertEquals("INVALID_ARGUMENT", (iae as Result.Failure).errors.first().code.value) - - val generic = Result.runCatching { throw Exception("x") } - assertTrue(generic is Result.Failure) - - val verrs = listOf(ValidationError.required("name"), ValidationError.invalidFormat("email")) - val fromVal: Result = Result.failure(verrs) - assertTrue(fromVal is Result.Failure) - assertEquals("REQUIRED", (fromVal as Result.Failure).errors.first().code.value) - - val rec = Result.failure(ErrorDto(ErrorCode("E"), "")).recover { _ -> "fallback" } - assertTrue(rec is Result.Success) - - val recFail = Result.failure(ErrorDto(ErrorCode("E"), "")).recoverCatching { _ -> throw IllegalStateException("boom") } - assertTrue(recFail is Result.Failure) - assertEquals("RECOVERY_FAILED", (recFail as Result.Failure).errors.first().code.value) - } - - @Test - fun `getOrNull default throw and toResult`() { - val s = Result.success(5) - assertEquals(5, s.getOrNull()) - - val f: Result = Result.failure(ErrorDto(ErrorCode("E"), "")) - assertNull(f.getOrNull()) - assertEquals(7, f.getOrDefault(7)) - - assertEquals(5, s.getOrThrow()) - try { - f.getOrThrow() - fail("should throw") - } catch (e: IllegalStateException) { - // ok - } - - val nullable: Int? = null - val r = nullable.toResult("ist leer") - assertTrue(r is Result.Failure) - - val r2 = 3.toResult() - assertTrue(r2 is Result.Success) - } + val r2 = 3.toResult() + assertTrue(r2 is Result.Success) + } } diff --git a/core/core-utils/src/jvmMain/kotlin/at/mocode/core/utils/DatabaseUtils.kt b/core/core-utils/src/jvmMain/kotlin/at/mocode/core/utils/DatabaseUtils.kt index 5b00fa3e..0e710cd1 100644 --- a/core/core-utils/src/jvmMain/kotlin/at/mocode/core/utils/DatabaseUtils.kt +++ b/core/core-utils/src/jvmMain/kotlin/at/mocode/core/utils/DatabaseUtils.kt @@ -1,12 +1,13 @@ package at.mocode.core.utils -import at.mocode.core.domain.model.ErrorCode +import at.mocode.core.domain.model.ErrorCodes import at.mocode.core.domain.model.ErrorDto import at.mocode.core.domain.model.PagedResponse import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.BatchInsertStatement import org.jetbrains.exposed.sql.transactions.transaction import java.sql.SQLException +import java.sql.SQLTimeoutException /** * JVM-specific database utilities for the Core module. @@ -22,61 +23,71 @@ import java.sql.SQLException * @return A Result containing either the operation result or error information */ inline fun transactionResult( - database: Database? = null, - crossinline block: Transaction.() -> T + database: Database? = null, + crossinline block: Transaction.() -> T ): Result { - return try { - val result = transaction(database) { block() } - Result.success(result) - } catch (e: SQLException) { - // Handle specific SQL exceptions - val errorCode = when { - e.message?.contains("constraint", ignoreCase = true) == true -> "CONSTRAINT_VIOLATION" - e.message?.contains("duplicate", ignoreCase = true) == true -> "DUPLICATE_ENTRY" - e.message?.contains("timeout", ignoreCase = true) == true -> "DATABASE_TIMEOUT" - else -> "DATABASE_ERROR" - } - - Result.failure( - ErrorDto( - code = ErrorCode(errorCode), - message = "Database operation failed: ${e.message}" - ) - ) - } catch (e: Exception) { - Result.failure( - ErrorDto( - code = ErrorCode("TRANSACTION_ERROR"), - message = "Transaction failed: ${e.message ?: "Unknown error"}" - ) - ) + return try { + val result = transaction(database) { block() } + Result.success(result) + } catch (e: SQLTimeoutException) { + Result.failure( + ErrorDto( + code = ErrorCodes.DATABASE_TIMEOUT, + message = "Datenbank-Operation wegen Timeout fehlgeschlagen" + ) + ) + } catch (e: SQLException) { + // Robustere Fehlerbehandlung über SQLSTATE (Postgres) + val mapped = when (e.sqlState) { + // unique_violation + "23505" -> ErrorCodes.DUPLICATE_ENTRY + // foreign_key_violation + "23503" -> ErrorCodes.FOREIGN_KEY_VIOLATION + // check_violation + "23514" -> ErrorCodes.CHECK_VIOLATION + else -> ErrorCodes.DATABASE_ERROR } + + Result.failure( + ErrorDto( + code = mapped, + message = "Datenbank-Operation fehlgeschlagen" + ) + ) + } catch (e: Exception) { + Result.failure( + ErrorDto( + code = ErrorCodes.TRANSACTION_ERROR, + message = "Transaktion fehlgeschlagen" + ) + ) + } } /** * Executes a write database operation. */ inline fun writeTransaction( - database: Database? = null, - crossinline block: Transaction.() -> T + database: Database? = null, + crossinline block: Transaction.() -> T ): Result = transactionResult(database, block) /** * Executes a read database operation. */ inline fun readTransaction( - database: Database? = null, - crossinline block: Transaction.() -> T + database: Database? = null, + crossinline block: Transaction.() -> T ): Result = transactionResult(database, block) /** * Extension function for Query-Builder to add pagination. */ fun Query.paginate(page: Int, size: Int): Query { - require(page >= 0) { "Page number must be non-negative" } - require(size > 0) { "Page size must be positive" } + require(page >= 0) { "Page number must be non-negative" } + require(size > 0) { "Page size must be positive" } - return limit(size).offset(start = (page * size).toLong()) + return limit(size).offset(start = (page * size).toLong()) } /** @@ -89,48 +100,48 @@ fun Query.paginate(page: Int, size: Int): Query { * @return A PagedResponse containing the paginated and transformed data */ fun Query.toPagedResponse( - page: Int, - size: Int, - transform: (ResultRow) -> T + page: Int, + size: Int, + transform: (ResultRow) -> T ): PagedResponse { - // Validate input parameters - require(page >= 0) { "Page number must be non-negative" } - require(size > 0) { "Page size must be positive" } + // Validate input parameters + require(page >= 0) { "Page number must be non-negative" } + require(size > 0) { "Page size must be positive" } - // Calculate the total count first (executes a COUNT query) - val totalCount = this.count() - - // If there are no results, return an empty page - if (totalCount == 0L) { - return PagedResponse.create( - content = emptyList(), - page = page, - size = size, - totalElements = 0, - totalPages = 0, - hasNext = false, - hasPrevious = page > 0 - ) - } - - // Calculate total pages - use ceil division to ensure we round up - val totalPages = ((totalCount + size - 1) / size).toInt() - - // Ensure the requested page exists (if page is beyond available pages, return the last page) - val adjustedPage = if (page >= totalPages) (totalPages - 1).coerceAtLeast(0) else page - - // Then apply pagination and transform results - val content = this.paginate(adjustedPage, size).map(transform) + // Calculate the total count first (executes a COUNT query) + val totalCount = this.count() + // If there are no results, return an empty page + if (totalCount == 0L) { return PagedResponse.create( - content = content, - page = adjustedPage, - size = size, - totalElements = totalCount, - totalPages = totalPages, - hasNext = adjustedPage < totalPages - 1, - hasPrevious = adjustedPage > 0 + content = emptyList(), + page = page, + size = size, + totalElements = 0, + totalPages = 0, + hasNext = false, + hasPrevious = page > 0 ) + } + + // Calculate total pages - use ceil division to ensure we round up + val totalPages = ((totalCount + size - 1) / size).toInt() + + // Ensure the requested page exists (if page is beyond available pages, return the last page) + val adjustedPage = if (page >= totalPages) (totalPages - 1).coerceAtLeast(0) else page + + // Then apply pagination and transform results + val content = this.paginate(adjustedPage, size).map(transform) + + return PagedResponse.create( + content = content, + page = adjustedPage, + size = size, + totalElements = totalCount, + totalPages = totalPages, + hasNext = adjustedPage < totalPages - 1, + hasPrevious = adjustedPage > 0 + ) } /** @@ -138,64 +149,93 @@ fun Query.toPagedResponse( */ object DatabaseUtils { - /** - * Checks if a table exists. - * Uses a safe query approach to verify table existence. - */ - fun tableExists(tableName: String, database: Database? = null): Boolean { - return try { - transaction(database) { - // Execute a safer SQL statement to check if table exists - val result = exec("SELECT 1 FROM information_schema.tables WHERE table_name = '$tableName' LIMIT 1") - // If the query returns a result, the table exists - result != null - } - } catch (e: Exception) { - false - } + /** + * Checks if a table exists. + * Uses a safe query approach to verify table existence. + */ + fun tableExists(tableName: String, database: Database? = null): Boolean { + return try { + transaction(database) { + // Postgres-spezifischer, robuster Ansatz über to_regclass + val valid = tableName.trim() + if (!valid.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) return@transaction false + exec("SELECT to_regclass('$valid')") { rs -> + if (rs.next()) rs.getString(1) else null + } != null + } + } catch (e: Exception) { + false } + } - /** - * Creates an index if it doesn't exist. - */ - fun createIndexIfNotExists( - tableName: String, - indexName: String, - columns: Array, - unique: Boolean = false, - database: Database? = null - ): Result { - return transactionResult(database) { - val uniqueStr = if (unique) "UNIQUE" else "" - val columnsStr = columns.joinToString(", ") - val sql = "CREATE $uniqueStr INDEX IF NOT EXISTS $indexName ON $tableName ($columnsStr)" - exec(sql) - } - } + /** + * Creates an index if it doesn't exist. + */ + @JvmName("createIndexIfNotExistsArray") + fun createIndexIfNotExists( + tableName: String, + indexName: String, + columns: Array, + unique: Boolean = false, + database: Database? = null + ): Result = createIndexIfNotExists(tableName, indexName, *columns, unique = unique, database = database) - /** - * Executes a raw SQL query and returns the number of affected rows. - */ - fun executeRawSql(sql: String, database: Database? = null): Result { - return transactionResult(database) { - (exec(sql) ?: 0) as Int - } - } + @JvmName("createIndexIfNotExistsVararg") + fun createIndexIfNotExists( + tableName: String, + indexName: String, + vararg columns: String, + unique: Boolean = false, + database: Database? = null + ): Result { + return transactionResult(database) { + // Einfache Sanitization + Quoting der Identifier + fun quoteIdent(name: String): String { + require(name.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) { "Ungültiger Identifier: $name" } + return "\"$name\"" + } - /** - * Helper function for batch inserts. - */ - inline fun batchInsert( - table: Table, - data: Iterable, - crossinline body: BatchInsertStatement.(T) -> Unit - ): Result> { - return transactionResult { - table.batchInsert(data) { item -> - body(item) - } - } + val uniqueStr = if (unique) "UNIQUE" else "" + val qTable = quoteIdent(tableName) + val qIndex = quoteIdent(indexName) + val cols = columns.map { quoteIdent(it) }.joinToString(", ") + val sql = "CREATE $uniqueStr INDEX IF NOT EXISTS $qIndex ON $qTable ($cols)" + exec(sql) + Unit } + } + + /** + * Führt ein beliebiges SQL-Statement aus (DDL/DML). Liefert keinen Update-Count zurück. + */ + fun executeRawSql(sql: String, database: Database? = null): Result = transactionResult(database) { + exec(sql) + Unit + } + + /** + * Executes a raw SQL update statement and returns affected rows. + */ + fun executeUpdate(sql: String, database: Database? = null): Result = transactionResult(database) { + // Nutzt Exposed PreparedStatementApi, kein AutoCloseable + val ps = this.connection.prepareStatement(sql, false) + ps.executeUpdate() + } + + /** + * Helper function for batch inserts. + */ + inline fun batchInsert( + table: Table, + data: Iterable, + crossinline body: BatchInsertStatement.(T) -> Unit + ): Result> { + return transactionResult { + table.batchInsert(data) { item -> + body(item) + } + } + } } /** @@ -206,11 +246,11 @@ object DatabaseUtils { * Safely gets a value from a ResultRow. */ fun ResultRow.getOrNull(column: Column): T? { - return try { - this[column] - } catch (e: Exception) { - null - } + return try { + this[column] + } catch (e: Exception) { + null + } } /** @@ -218,17 +258,17 @@ fun ResultRow.getOrNull(column: Column): T? { * Safely handles any exceptions during the conversion process. */ fun ResultRow.toMap(): Map { - val result = mutableMapOf() - this.fieldIndex.forEach { (expression, _) -> - try { - when (expression) { - is Column<*> -> result[expression.name] = this[expression] - else -> result[expression.toString()] = this[expression] - } - } catch (e: Exception) { - // Ignore columns that can't be read and log the error if needed - // You could add logging here in a production environment - } + val result = mutableMapOf() + this.fieldIndex.forEach { (expression, _) -> + try { + when (expression) { + is Column<*> -> result[expression.name] = this[expression] + else -> result[expression.toString()] = this[expression] + } + } catch (e: Exception) { + // Ignore columns that can't be read and log the error if needed + // You could add logging here in a production environment } - return result + } + return result } diff --git a/docker/core/keycloak/meldestelle-realm.json b/docker/core/keycloak/meldestelle-realm.json index eee5bcf0..06a37131 100644 --- a/docker/core/keycloak/meldestelle-realm.json +++ b/docker/core/keycloak/meldestelle-realm.json @@ -182,10 +182,12 @@ "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, "redirectUris": [ + "http://localhost:4000/*", "http://localhost:3000/*", "https://app.meldestelle.at/*" ], "webOrigins": [ + "http://localhost:4000", "http://localhost:3000", "https://app.meldestelle.at" ], diff --git a/dockerfiles/clients/desktop-app/Dockerfile b/dockerfiles/clients/desktop-app/Dockerfile index b4237957..09ada4d9 100644 --- a/dockerfiles/clients/desktop-app/Dockerfile +++ b/dockerfiles/clients/desktop-app/Dockerfile @@ -17,6 +17,7 @@ COPY gradlew ./ # Kopiere alle notwendigen Module für Multi-Modul-Projekt COPY clients ./clients COPY core ./core +COPY domains ./domains COPY platform ./platform COPY infrastructure ./infrastructure COPY services ./services diff --git a/dockerfiles/clients/web-app/Dockerfile b/dockerfiles/clients/web-app/Dockerfile index 552b2d17..951ef685 100644 --- a/dockerfiles/clients/web-app/Dockerfile +++ b/dockerfiles/clients/web-app/Dockerfile @@ -9,6 +9,9 @@ ARG GRADLE_VERSION ARG JAVA_VERSION ARG NODE_VERSION +ARG NGINX_IMAGE_TAG=1.28.0-alpine +# Toggle build profile: dev (default) or prod +ARG WEB_BUILD_PROFILE=dev FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION} AS builder # Install Node.js (version aligned with versions.toml) @@ -29,6 +32,7 @@ COPY gradlew ./ # Kopiere alle notwendigen Module für Multi-Modul-Projekt COPY clients ./clients COPY core ./core +COPY domains ./domains COPY platform ./platform COPY infrastructure ./infrastructure COPY services ./services @@ -40,21 +44,28 @@ RUN chmod +x ./gradlew # Dependencies downloaden (für besseres Caching) RUN ./gradlew :clients:app:dependencies --no-configure-on-demand -# Kotlin/JS Web-App kompilieren (PRODUCTION Build) -RUN ./gradlew :clients:app:jsBrowserDistribution --no-configure-on-demand -Pproduction=true +# Kotlin/JS Web-App kompilieren (Profil wählbar über WEB_BUILD_PROFILE) +# - dev → jsBrowserDevelopmentExecutable (schneller, Source Maps) +# - prod → jsBrowserDistribution (minifiziert, optimiert) +RUN if [ "$WEB_BUILD_PROFILE" = "prod" ]; then \ + ./gradlew :clients:app:jsBrowserDistribution --no-configure-on-demand -Pproduction=true; \ + mkdir -p /app/web-dist && cp -r clients/app/build/dist/js/productionExecutable/* /app/web-dist/; \ + else \ + ./gradlew :clients:app:jsBrowserDevelopmentExecutable --no-configure-on-demand; \ + mkdir -p /app/web-dist && cp -r clients/app/build/dist/js/developmentExecutable/* /app/web-dist/; \ + fi # =================================================================== # Stage 2: Runtime Stage - Nginx für Static Files + API Proxy # =================================================================== -# Build arg controls runtime base image tag (build-time only) -ARG NGINX_IMAGE_TAG +# Build arg controls runtime base image tag (declared globally to allow usage in FROM) FROM nginx:${NGINX_IMAGE_TAG} # Installiere curl für Health-Checks RUN apk add --no-cache curl -# Kopiere kompilierte Web-App von Build-Stage -COPY --from=builder /app/clients/app/build/dist/js/productionExecutable/ /usr/share/nginx/html/ +# Kopiere kompilierte Web-App von Build-Stage (vereinheitlichtes Ausgabeverzeichnis) +COPY --from=builder /app/web-dist/ /usr/share/nginx/html/ # Kopiere Nginx-Konfiguration COPY dockerfiles/clients/web-app/nginx.conf /etc/nginx/nginx.conf @@ -62,6 +73,9 @@ COPY dockerfiles/clients/web-app/nginx.conf /etc/nginx/nginx.conf # Exponiere Port 4000 (statt Standard 80) EXPOSE 4000 +# Downloads (Platzhalter) ausliefern lassen +COPY dockerfiles/clients/web-app/downloads/ /usr/share/nginx/html/downloads/ + # Health-Check für Container HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ CMD curl -f http://localhost:4000/ || exit 1 diff --git a/dockerfiles/clients/web-app/downloads/index.html b/dockerfiles/clients/web-app/downloads/index.html new file mode 100644 index 00000000..540a37a8 --- /dev/null +++ b/dockerfiles/clients/web-app/downloads/index.html @@ -0,0 +1,30 @@ + + + + + + Meldestelle – Desktop Downloads (Platzhalter) + + + +

Desktop Downloads

+

Platzhalter-Verzeichnis. Hier können zukünftig Installer/Archive der Desktop-App bereitgestellt werden.

+
+

Lege deine Dateien in dieses Verzeichnis im Repository:

+
dockerfiles/clients/web-app/downloads/
+

Oder mounte in Docker Compose ein Host-Verzeichnis auf /usr/share/nginx/html/downloads.

+

Beispiele (geplant):

+
    +
  • Meldestelle-Setup-1.0.0.msi (Windows)
  • +
  • Meldestelle-1.0.0.dmg (macOS)
  • +
  • Meldestelle-1.0.0.deb (Linux)
  • +
+
+ + diff --git a/dockerfiles/services/events-service/Dockerfile b/dockerfiles/domains/events-service/Dockerfile similarity index 100% rename from dockerfiles/services/events-service/Dockerfile rename to dockerfiles/domains/events-service/Dockerfile diff --git a/dockerfiles/services/horses-service/Dockerfile b/dockerfiles/domains/horses-service/Dockerfile similarity index 100% rename from dockerfiles/services/horses-service/Dockerfile rename to dockerfiles/domains/horses-service/Dockerfile diff --git a/dockerfiles/services/masterdata-service/Dockerfile b/dockerfiles/domains/masterdata-service/Dockerfile similarity index 100% rename from dockerfiles/services/masterdata-service/Dockerfile rename to dockerfiles/domains/masterdata-service/Dockerfile diff --git a/dockerfiles/services/members-service/Dockerfile b/dockerfiles/domains/members-service/Dockerfile similarity index 100% rename from dockerfiles/services/members-service/Dockerfile rename to dockerfiles/domains/members-service/Dockerfile diff --git a/dockerfiles/infrastructure/gateway/Dockerfile b/dockerfiles/infrastructure/gateway/Dockerfile index bf8a3eeb..736b558b 100644 --- a/dockerfiles/infrastructure/gateway/Dockerfile +++ b/dockerfiles/infrastructure/gateway/Dockerfile @@ -60,6 +60,9 @@ COPY core/ core/ # Copy infrastructure directories (required by settings.gradle.kts) COPY infrastructure/ infrastructure/ +# Copy domains directory (required by settings.gradle.kts) +COPY domains/ domains/ + # Copy services directories (required by settings.gradle.kts) COPY services/ services/ diff --git a/dockerfiles/services/ping-service/Dockerfile b/dockerfiles/services/ping-service/Dockerfile index 51fa6107..8a012d29 100644 --- a/dockerfiles/services/ping-service/Dockerfile +++ b/dockerfiles/services/ping-service/Dockerfile @@ -60,6 +60,9 @@ COPY core/ core/ # Copy infrastructure directories (required by settings.gradle.kts) COPY infrastructure/ infrastructure/ +# Copy domains directory (required by settings.gradle.kts) +COPY domains/ domains/ + # Copy services directories (required by settings.gradle.kts) COPY services/ services/ diff --git a/docs/clients/Architektur.md b/docs/clients/Architektur.md new file mode 100644 index 00000000..6e48bffd --- /dev/null +++ b/docs/clients/Architektur.md @@ -0,0 +1,86 @@ +# Meldestelle Clients – Architekturübersicht (aktualisiert) + +## Ziele +- Öffentliche Willkommensseite mit Links zu Ping-Service, Keycloak Login/Registrierung +- Einfache Auth-Status-Seite („Du bist als … angemeldet“) +- Bereinigung der Legacy-UI (altes Präsentations-Layer entfernt) +- Bereitstellung als Docker-Container: `web-app` (Kotlin/JS) und `desktop-app` (VNC/noVNC) + +## Module und Struktur +- `clients/app`: Einstieg der Anwendung (Compose Multiplatform) + - `MainApp.kt`: Start-Routing, Composables `WelcomeScreen`, `LoginScreen`, `AuthStatusScreen` + - Verwendet: `ping-feature` (PingScreen), `auth-feature` (Login via `AuthApiClient`) +- `clients/shared`: Gemeinsame Domain/Data/DI + `AppConfig` + - `AppConfig`: Basis-URLs und Keycloak-Client-Konfiguration +- `clients/shared/common-ui`: generische UI-Bausteine (Legacy-Teile neutralisiert) + - `layout/MainLayout.kt`, `components/NotificationCard.kt`, `screens/DashboardScreen.kt` → bewusst geleert +- `clients/auth-feature`: Login-API gegen Keycloak (Password Grant) +- `clients/ping-feature`: Ping-Screen und ViewModel, greift auf Ping-Service zu + +## Navigation (vereinfacht) +- Start: `AppScreen.Home` → `WelcomeScreen` +- `AppScreen.Ping` → `PingScreen` +- `AppScreen.Login` → `LoginScreen` +- `AppScreen.Profile` → `AuthStatusScreen` + +## Keycloak-Konfiguration +Quelle: `docker/core/keycloak/meldestelle-realm.json` und `docs/reference/ports-and-urls.md` +- Realm: `meldestelle` +- Öffentlicher Client: `web-app` (PKCE; für Browser) +- Keycloak URL (lokal): `http://localhost:8180` +- AppConfig nutzt: `KEYCLOAK_URL=http://localhost:8180`, `KEYCLOAK_REALM=meldestelle`, `KEYCLOAK_CLIENT_ID=web-app` + +### Authentifizierungs-Flow (PKCE) +- Flow: Authorization Code Flow mit PKCE (S256) +- Redirect-URI (lokal): `http://localhost:4000/` (Root, Query-Parameter werden vom Client ausgewertet) +- Ablauf: + 1. Button „Login“ → PKCE-Start (Code Verifier/Challenge werden generiert) und Redirect zu Keycloak `/auth` + 2. Keycloak leitet zurück auf `http://localhost:4000/?code=...&state=...` + 3. Client tauscht `code` + `code_verifier` am `/token`-Endpoint gegen Tokens + 4. `AuthTokenManager` speichert Access-Token (in-memory), UI zeigt Status „Du bist als … angemeldet“ + +Hinweis: Password-Grant wird nicht mehr genutzt; für Desktop (JVM) bleibt bei Bedarf der lokale Login-Screen als Fallback vorhanden. + +## Ports und URLs (lokal) +Quelle: `docs/reference/ports-and-urls.md` +- API Gateway: `http://localhost:8081` +- Keycloak: `http://localhost:8180` +- Ping Service: `http://localhost:8082` +- Web App: `http://localhost:4000` +- Desktop App: VNC `5901`, noVNC `http://localhost:6080` + +## Docker +### Web-App (Kotlin/JS, kein WASM) +- Dockerfile: `dockerfiles/clients/web-app/Dockerfile` +- Build: Gradle `:clients:app:jsBrowserDistribution` → statische Dateien via Nginx +- Compose: + - Hardcoded: Service `web-app` mit Port `4000:4000` in `compose.hardcoded.yaml` + - Variablen: Service `web-app` mit `${WEB_APP_PORT}` in `compose.yaml` (Wert in `.env`) + +Downloads (Desktop-Installer, Platzhalter): +- Verzeichnis: `dockerfiles/clients/web-app/downloads/` → wird nach `/usr/share/nginx/html/downloads/` kopiert +- URL: `http://localhost:4000/downloads/` +- Alternativ: per Compose ein Host-Verzeichnis auf `/usr/share/nginx/html/downloads` mounten + +### Desktop-App (VNC/noVNC) +- Dockerfile: `dockerfiles/clients/desktop-app/Dockerfile` +- Build: Gradle `:clients:app:createDistributable` → Desktop Runtime +- Runtime: Ubuntu + `xvfb` + `x11vnc` + `noVNC` + `supervisord` +- Compose: + - Hardcoded: Service `desktop-app` 5901/6080 in `compose.hardcoded.yaml` + - Variablen: Service `desktop-app` mit `${DESKTOP_APP_VNC_PORT}` und `${DESKTOP_APP_NOVNC_PORT}` in `compose.yaml` (Werte in `.env`) + +## Bereinigung (Altlasten) +- Entfernt/neutralisiert: Altes Präsentations-Layer (abhängig von `presentation.state`/`actions`) + - `clients/shared/common-ui/components/NotificationCard.kt` → geleert + - `clients/shared/common-ui/layout/MainLayout.kt` → geleert + - `clients/shared/common-ui/screens/DashboardScreen.kt` → geleert + +## Konfigurationsquelle +- Einheitliche Werte/Ports: `docker/versions.toml` (Clients: `web-app=4000`, `desktop-app-vnc=5901`, `desktop-app-novnc=6080`) +- Compose-Variablen: `.env` und `.env.template` + +## Nächste Schritte +- Optional: Umstellung Login auf Authorization Code Flow (PKCE) für Browser +- Optional: Willkommensseite visuell ausbauen (Branding) +- Optional: Bereitstellung der Desktop-Installer über `web-app` Download-Link diff --git a/services/events/README-EVENTS.md b/domains/events/README-EVENTS.md similarity index 100% rename from services/events/README-EVENTS.md rename to domains/events/README-EVENTS.md diff --git a/services/events/events-api/build.gradle.kts b/domains/events/events-api/build.gradle.kts similarity index 100% rename from services/events/events-api/build.gradle.kts rename to domains/events/events-api/build.gradle.kts diff --git a/services/events/events-api/src/main/kotlin/at/mocode/events/api/rest/VeranstaltungController.kt b/domains/events/events-api/src/main/kotlin/at/mocode/events/api/rest/VeranstaltungController.kt similarity index 100% rename from services/events/events-api/src/main/kotlin/at/mocode/events/api/rest/VeranstaltungController.kt rename to domains/events/events-api/src/main/kotlin/at/mocode/events/api/rest/VeranstaltungController.kt diff --git a/services/events/events-application/build.gradle.kts b/domains/events/events-common/build.gradle.kts similarity index 100% rename from services/events/events-application/build.gradle.kts rename to domains/events/events-common/build.gradle.kts diff --git a/services/events/events-application/src/main/kotlin/at/mocode/events/application/usecase/CreateVeranstaltungUseCase.kt b/domains/events/events-common/src/main/kotlin/at/mocode/events/application/usecase/CreateVeranstaltungUseCase.kt similarity index 100% rename from services/events/events-application/src/main/kotlin/at/mocode/events/application/usecase/CreateVeranstaltungUseCase.kt rename to domains/events/events-common/src/main/kotlin/at/mocode/events/application/usecase/CreateVeranstaltungUseCase.kt diff --git a/services/events/events-application/src/main/kotlin/at/mocode/events/application/usecase/DeleteVeranstaltungUseCase.kt b/domains/events/events-common/src/main/kotlin/at/mocode/events/application/usecase/DeleteVeranstaltungUseCase.kt similarity index 100% rename from services/events/events-application/src/main/kotlin/at/mocode/events/application/usecase/DeleteVeranstaltungUseCase.kt rename to domains/events/events-common/src/main/kotlin/at/mocode/events/application/usecase/DeleteVeranstaltungUseCase.kt diff --git a/services/events/events-application/src/main/kotlin/at/mocode/events/application/usecase/GetVeranstaltungUseCase.kt b/domains/events/events-common/src/main/kotlin/at/mocode/events/application/usecase/GetVeranstaltungUseCase.kt similarity index 100% rename from services/events/events-application/src/main/kotlin/at/mocode/events/application/usecase/GetVeranstaltungUseCase.kt rename to domains/events/events-common/src/main/kotlin/at/mocode/events/application/usecase/GetVeranstaltungUseCase.kt diff --git a/services/events/events-application/src/main/kotlin/at/mocode/events/application/usecase/UpdateVeranstaltungUseCase.kt b/domains/events/events-common/src/main/kotlin/at/mocode/events/application/usecase/UpdateVeranstaltungUseCase.kt similarity index 100% rename from services/events/events-application/src/main/kotlin/at/mocode/events/application/usecase/UpdateVeranstaltungUseCase.kt rename to domains/events/events-common/src/main/kotlin/at/mocode/events/application/usecase/UpdateVeranstaltungUseCase.kt diff --git a/services/events/events-domain/build.gradle.kts b/domains/events/events-domain/build.gradle.kts similarity index 100% rename from services/events/events-domain/build.gradle.kts rename to domains/events/events-domain/build.gradle.kts diff --git a/services/events/events-domain/src/main/kotlin/at/mocode/events/EventManagement.kt b/domains/events/events-domain/src/main/kotlin/at/mocode/events/EventManagement.kt similarity index 100% rename from services/events/events-domain/src/main/kotlin/at/mocode/events/EventManagement.kt rename to domains/events/events-domain/src/main/kotlin/at/mocode/events/EventManagement.kt diff --git a/services/events/events-domain/src/main/kotlin/at/mocode/events/domain/model/Veranstaltung.kt b/domains/events/events-domain/src/main/kotlin/at/mocode/events/domain/model/Veranstaltung.kt similarity index 100% rename from services/events/events-domain/src/main/kotlin/at/mocode/events/domain/model/Veranstaltung.kt rename to domains/events/events-domain/src/main/kotlin/at/mocode/events/domain/model/Veranstaltung.kt diff --git a/services/events/events-domain/src/main/kotlin/at/mocode/events/domain/repository/VeranstaltungRepository.kt b/domains/events/events-domain/src/main/kotlin/at/mocode/events/domain/repository/VeranstaltungRepository.kt similarity index 100% rename from services/events/events-domain/src/main/kotlin/at/mocode/events/domain/repository/VeranstaltungRepository.kt rename to domains/events/events-domain/src/main/kotlin/at/mocode/events/domain/repository/VeranstaltungRepository.kt diff --git a/services/events/events-infrastructure/build.gradle.kts b/domains/events/events-infrastructure/build.gradle.kts similarity index 100% rename from services/events/events-infrastructure/build.gradle.kts rename to domains/events/events-infrastructure/build.gradle.kts diff --git a/services/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungRepositoryImpl.kt b/domains/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungRepositoryImpl.kt similarity index 100% rename from services/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungRepositoryImpl.kt rename to domains/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungRepositoryImpl.kt diff --git a/services/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungTable.kt b/domains/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungTable.kt similarity index 100% rename from services/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungTable.kt rename to domains/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungTable.kt diff --git a/services/events/events-service/build.gradle.kts b/domains/events/events-service/build.gradle.kts similarity index 100% rename from services/events/events-service/build.gradle.kts rename to domains/events/events-service/build.gradle.kts diff --git a/services/events/events-service/src/main/kotlin/at/mocode/events/service/EventsServiceApplication.kt b/domains/events/events-service/src/main/kotlin/at/mocode/events/service/EventsServiceApplication.kt similarity index 100% rename from services/events/events-service/src/main/kotlin/at/mocode/events/service/EventsServiceApplication.kt rename to domains/events/events-service/src/main/kotlin/at/mocode/events/service/EventsServiceApplication.kt diff --git a/services/events/events-service/src/main/kotlin/at/mocode/events/service/config/EventsDatabaseConfiguration.kt b/domains/events/events-service/src/main/kotlin/at/mocode/events/service/config/EventsDatabaseConfiguration.kt similarity index 100% rename from services/events/events-service/src/main/kotlin/at/mocode/events/service/config/EventsDatabaseConfiguration.kt rename to domains/events/events-service/src/main/kotlin/at/mocode/events/service/config/EventsDatabaseConfiguration.kt diff --git a/services/events/events-service/src/test/resources/logback-test.xml b/domains/events/events-service/src/test/resources/logback-test.xml similarity index 100% rename from services/events/events-service/src/test/resources/logback-test.xml rename to domains/events/events-service/src/test/resources/logback-test.xml diff --git a/services/horses/README-HORSES.md b/domains/horses/README-HORSES.md similarity index 100% rename from services/horses/README-HORSES.md rename to domains/horses/README-HORSES.md diff --git a/services/horses/horses-api/build.gradle.kts b/domains/horses/horses-api/build.gradle.kts similarity index 100% rename from services/horses/horses-api/build.gradle.kts rename to domains/horses/horses-api/build.gradle.kts diff --git a/services/horses/horses-api/src/main/kotlin/at/mocode/horses/api/rest/HorseController.kt b/domains/horses/horses-api/src/main/kotlin/at/mocode/horses/api/rest/HorseController.kt similarity index 100% rename from services/horses/horses-api/src/main/kotlin/at/mocode/horses/api/rest/HorseController.kt rename to domains/horses/horses-api/src/main/kotlin/at/mocode/horses/api/rest/HorseController.kt diff --git a/services/horses/horses-application/build.gradle.kts b/domains/horses/horses-common/build.gradle.kts similarity index 100% rename from services/horses/horses-application/build.gradle.kts rename to domains/horses/horses-common/build.gradle.kts diff --git a/services/horses/horses-application/src/main/kotlin/at/mocode/horses/application/usecase/CreateHorseUseCase.kt b/domains/horses/horses-common/src/main/kotlin/at/mocode/horses/application/usecase/CreateHorseUseCase.kt similarity index 100% rename from services/horses/horses-application/src/main/kotlin/at/mocode/horses/application/usecase/CreateHorseUseCase.kt rename to domains/horses/horses-common/src/main/kotlin/at/mocode/horses/application/usecase/CreateHorseUseCase.kt diff --git a/services/horses/horses-application/src/main/kotlin/at/mocode/horses/application/usecase/DeleteHorseUseCase.kt b/domains/horses/horses-common/src/main/kotlin/at/mocode/horses/application/usecase/DeleteHorseUseCase.kt similarity index 100% rename from services/horses/horses-application/src/main/kotlin/at/mocode/horses/application/usecase/DeleteHorseUseCase.kt rename to domains/horses/horses-common/src/main/kotlin/at/mocode/horses/application/usecase/DeleteHorseUseCase.kt diff --git a/services/horses/horses-application/src/main/kotlin/at/mocode/horses/application/usecase/GetHorseUseCase.kt b/domains/horses/horses-common/src/main/kotlin/at/mocode/horses/application/usecase/GetHorseUseCase.kt similarity index 100% rename from services/horses/horses-application/src/main/kotlin/at/mocode/horses/application/usecase/GetHorseUseCase.kt rename to domains/horses/horses-common/src/main/kotlin/at/mocode/horses/application/usecase/GetHorseUseCase.kt diff --git a/services/horses/horses-application/src/main/kotlin/at/mocode/horses/application/usecase/TransactionalCreateHorseUseCase.kt b/domains/horses/horses-common/src/main/kotlin/at/mocode/horses/application/usecase/TransactionalCreateHorseUseCase.kt similarity index 100% rename from services/horses/horses-application/src/main/kotlin/at/mocode/horses/application/usecase/TransactionalCreateHorseUseCase.kt rename to domains/horses/horses-common/src/main/kotlin/at/mocode/horses/application/usecase/TransactionalCreateHorseUseCase.kt diff --git a/services/horses/horses-application/src/main/kotlin/at/mocode/horses/application/usecase/UpdateHorseUseCase.kt b/domains/horses/horses-common/src/main/kotlin/at/mocode/horses/application/usecase/UpdateHorseUseCase.kt similarity index 100% rename from services/horses/horses-application/src/main/kotlin/at/mocode/horses/application/usecase/UpdateHorseUseCase.kt rename to domains/horses/horses-common/src/main/kotlin/at/mocode/horses/application/usecase/UpdateHorseUseCase.kt diff --git a/services/horses/horses-domain/build.gradle.kts b/domains/horses/horses-domain/build.gradle.kts similarity index 100% rename from services/horses/horses-domain/build.gradle.kts rename to domains/horses/horses-domain/build.gradle.kts diff --git a/services/horses/horses-domain/src/main/kotlin/at/mocode/horses/domain/model/DomPferd.kt b/domains/horses/horses-domain/src/main/kotlin/at/mocode/horses/domain/model/DomPferd.kt similarity index 100% rename from services/horses/horses-domain/src/main/kotlin/at/mocode/horses/domain/model/DomPferd.kt rename to domains/horses/horses-domain/src/main/kotlin/at/mocode/horses/domain/model/DomPferd.kt diff --git a/services/horses/horses-domain/src/main/kotlin/at/mocode/horses/domain/repository/HorseRepository.kt b/domains/horses/horses-domain/src/main/kotlin/at/mocode/horses/domain/repository/HorseRepository.kt similarity index 100% rename from services/horses/horses-domain/src/main/kotlin/at/mocode/horses/domain/repository/HorseRepository.kt rename to domains/horses/horses-domain/src/main/kotlin/at/mocode/horses/domain/repository/HorseRepository.kt diff --git a/services/horses/horses-infrastructure/build.gradle.kts b/domains/horses/horses-infrastructure/build.gradle.kts similarity index 100% rename from services/horses/horses-infrastructure/build.gradle.kts rename to domains/horses/horses-infrastructure/build.gradle.kts diff --git a/services/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseRepositoryImpl.kt b/domains/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseRepositoryImpl.kt similarity index 100% rename from services/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseRepositoryImpl.kt rename to domains/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseRepositoryImpl.kt diff --git a/services/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseTable.kt b/domains/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseTable.kt similarity index 100% rename from services/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseTable.kt rename to domains/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseTable.kt diff --git a/services/horses/horses-service/build.gradle.kts b/domains/horses/horses-service/build.gradle.kts similarity index 100% rename from services/horses/horses-service/build.gradle.kts rename to domains/horses/horses-service/build.gradle.kts diff --git a/services/horses/horses-service/src/main/kotlin/at/mocode/horses/service/HorsesServiceApplication.kt b/domains/horses/horses-service/src/main/kotlin/at/mocode/horses/service/HorsesServiceApplication.kt similarity index 100% rename from services/horses/horses-service/src/main/kotlin/at/mocode/horses/service/HorsesServiceApplication.kt rename to domains/horses/horses-service/src/main/kotlin/at/mocode/horses/service/HorsesServiceApplication.kt diff --git a/services/horses/horses-service/src/main/kotlin/at/mocode/horses/service/config/ApplicationConfiguration.kt b/domains/horses/horses-service/src/main/kotlin/at/mocode/horses/service/config/ApplicationConfiguration.kt similarity index 100% rename from services/horses/horses-service/src/main/kotlin/at/mocode/horses/service/config/ApplicationConfiguration.kt rename to domains/horses/horses-service/src/main/kotlin/at/mocode/horses/service/config/ApplicationConfiguration.kt diff --git a/services/horses/horses-service/src/main/kotlin/at/mocode/horses/service/config/DatabaseConfiguration.kt b/domains/horses/horses-service/src/main/kotlin/at/mocode/horses/service/config/DatabaseConfiguration.kt similarity index 100% rename from services/horses/horses-service/src/main/kotlin/at/mocode/horses/service/config/DatabaseConfiguration.kt rename to domains/horses/horses-service/src/main/kotlin/at/mocode/horses/service/config/DatabaseConfiguration.kt diff --git a/services/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/HorseServiceIntegrationTest.kt b/domains/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/HorseServiceIntegrationTest.kt similarity index 100% rename from services/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/HorseServiceIntegrationTest.kt rename to domains/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/HorseServiceIntegrationTest.kt diff --git a/services/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/TransactionContextTest.kt b/domains/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/TransactionContextTest.kt similarity index 100% rename from services/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/TransactionContextTest.kt rename to domains/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/TransactionContextTest.kt diff --git a/services/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/TransactionalContextTest.kt b/domains/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/TransactionalContextTest.kt similarity index 100% rename from services/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/TransactionalContextTest.kt rename to domains/horses/horses-service/src/test/kotlin/at/mocode/horses/service/integration/TransactionalContextTest.kt diff --git a/services/horses/horses-service/src/test/resources/logback-test.xml b/domains/horses/horses-service/src/test/resources/logback-test.xml similarity index 100% rename from services/horses/horses-service/src/test/resources/logback-test.xml rename to domains/horses/horses-service/src/test/resources/logback-test.xml diff --git a/services/masterdata/README-MASTERDATA.md b/domains/masterdata/README-MASTERDATA.md similarity index 100% rename from services/masterdata/README-MASTERDATA.md rename to domains/masterdata/README-MASTERDATA.md diff --git a/services/masterdata/masterdata-api/build.gradle.kts b/domains/masterdata/masterdata-api/build.gradle.kts similarity index 100% rename from services/masterdata/masterdata-api/build.gradle.kts rename to domains/masterdata/masterdata-api/build.gradle.kts diff --git a/services/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/StatusPages.kt b/domains/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/StatusPages.kt similarity index 100% rename from services/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/StatusPages.kt rename to domains/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/StatusPages.kt diff --git a/services/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/AltersklasseController.kt b/domains/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/AltersklasseController.kt similarity index 100% rename from services/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/AltersklasseController.kt rename to domains/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/AltersklasseController.kt diff --git a/services/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/BundeslandController.kt b/domains/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/BundeslandController.kt similarity index 100% rename from services/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/BundeslandController.kt rename to domains/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/BundeslandController.kt diff --git a/services/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/CountryController.kt b/domains/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/CountryController.kt similarity index 100% rename from services/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/CountryController.kt rename to domains/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/CountryController.kt diff --git a/services/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/PlatzController.kt b/domains/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/PlatzController.kt similarity index 100% rename from services/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/PlatzController.kt rename to domains/masterdata/masterdata-api/src/main/kotlin/at/mocode/masterdata/api/rest/PlatzController.kt diff --git a/services/masterdata/masterdata-application/build.gradle.kts b/domains/masterdata/masterdata-common/build.gradle.kts similarity index 100% rename from services/masterdata/masterdata-application/build.gradle.kts rename to domains/masterdata/masterdata-common/build.gradle.kts diff --git a/services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateAltersklasseUseCase.kt b/domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateAltersklasseUseCase.kt similarity index 100% rename from services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateAltersklasseUseCase.kt rename to domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateAltersklasseUseCase.kt diff --git a/services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateBundeslandUseCase.kt b/domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateBundeslandUseCase.kt similarity index 100% rename from services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateBundeslandUseCase.kt rename to domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateBundeslandUseCase.kt diff --git a/services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateCountryUseCase.kt b/domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateCountryUseCase.kt similarity index 100% rename from services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateCountryUseCase.kt rename to domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/CreateCountryUseCase.kt diff --git a/services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/CreatePlatzUseCase.kt b/domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/CreatePlatzUseCase.kt similarity index 100% rename from services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/CreatePlatzUseCase.kt rename to domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/CreatePlatzUseCase.kt diff --git a/services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/GetAltersklasseUseCase.kt b/domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/GetAltersklasseUseCase.kt similarity index 100% rename from services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/GetAltersklasseUseCase.kt rename to domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/GetAltersklasseUseCase.kt diff --git a/services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/GetBundeslandUseCase.kt b/domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/GetBundeslandUseCase.kt similarity index 100% rename from services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/GetBundeslandUseCase.kt rename to domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/GetBundeslandUseCase.kt diff --git a/services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/GetCountryUseCase.kt b/domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/GetCountryUseCase.kt similarity index 100% rename from services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/GetCountryUseCase.kt rename to domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/GetCountryUseCase.kt diff --git a/services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/GetPlatzUseCase.kt b/domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/GetPlatzUseCase.kt similarity index 100% rename from services/masterdata/masterdata-application/src/main/kotlin/at/mocode/masterdata/application/usecase/GetPlatzUseCase.kt rename to domains/masterdata/masterdata-common/src/main/kotlin/at/mocode/masterdata/application/usecase/GetPlatzUseCase.kt diff --git a/services/masterdata/masterdata-domain/build.gradle.kts b/domains/masterdata/masterdata-domain/build.gradle.kts similarity index 100% rename from services/masterdata/masterdata-domain/build.gradle.kts rename to domains/masterdata/masterdata-domain/build.gradle.kts diff --git a/services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/AltersklasseDefinition.kt b/domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/AltersklasseDefinition.kt similarity index 100% rename from services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/AltersklasseDefinition.kt rename to domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/AltersklasseDefinition.kt diff --git a/services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/BundeslandDefinition.kt b/domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/BundeslandDefinition.kt similarity index 100% rename from services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/BundeslandDefinition.kt rename to domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/BundeslandDefinition.kt diff --git a/services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/LandDefinition.kt b/domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/LandDefinition.kt similarity index 100% rename from services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/LandDefinition.kt rename to domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/LandDefinition.kt diff --git a/services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/Platz.kt b/domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/Platz.kt similarity index 100% rename from services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/Platz.kt rename to domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/model/Platz.kt diff --git a/services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/AltersklasseRepository.kt b/domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/AltersklasseRepository.kt similarity index 100% rename from services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/AltersklasseRepository.kt rename to domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/AltersklasseRepository.kt diff --git a/services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/BundeslandRepository.kt b/domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/BundeslandRepository.kt similarity index 100% rename from services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/BundeslandRepository.kt rename to domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/BundeslandRepository.kt diff --git a/services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/LandRepository.kt b/domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/LandRepository.kt similarity index 100% rename from services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/LandRepository.kt rename to domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/LandRepository.kt diff --git a/services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/PlatzRepository.kt b/domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/PlatzRepository.kt similarity index 100% rename from services/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/PlatzRepository.kt rename to domains/masterdata/masterdata-domain/src/main/kotlin/at/mocode/masterdata/domain/repository/PlatzRepository.kt diff --git a/services/masterdata/masterdata-infrastructure/build.gradle.kts b/domains/masterdata/masterdata-infrastructure/build.gradle.kts similarity index 100% rename from services/masterdata/masterdata-infrastructure/build.gradle.kts rename to domains/masterdata/masterdata-infrastructure/build.gradle.kts diff --git a/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseRepositoryImpl.kt b/domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseRepositoryImpl.kt similarity index 100% rename from services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseRepositoryImpl.kt rename to domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseRepositoryImpl.kt diff --git a/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt b/domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt similarity index 100% rename from services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt rename to domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt diff --git a/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt b/domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt similarity index 100% rename from services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt rename to domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt diff --git a/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt b/domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt similarity index 100% rename from services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt rename to domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt diff --git a/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandRepositoryImpl.kt b/domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandRepositoryImpl.kt similarity index 100% rename from services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandRepositoryImpl.kt rename to domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandRepositoryImpl.kt diff --git a/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandTable.kt b/domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandTable.kt similarity index 100% rename from services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandTable.kt rename to domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandTable.kt diff --git a/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzRepositoryImpl.kt b/domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzRepositoryImpl.kt similarity index 100% rename from services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzRepositoryImpl.kt rename to domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzRepositoryImpl.kt diff --git a/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzTable.kt b/domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzTable.kt similarity index 100% rename from services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzTable.kt rename to domains/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzTable.kt diff --git a/services/masterdata/masterdata-service/build.gradle.kts b/domains/masterdata/masterdata-service/build.gradle.kts similarity index 100% rename from services/masterdata/masterdata-service/build.gradle.kts rename to domains/masterdata/masterdata-service/build.gradle.kts diff --git a/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/MasterdataServiceApplication.kt b/domains/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/MasterdataServiceApplication.kt similarity index 100% rename from services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/MasterdataServiceApplication.kt rename to domains/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/MasterdataServiceApplication.kt diff --git a/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataConfiguration.kt b/domains/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataConfiguration.kt similarity index 100% rename from services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataConfiguration.kt rename to domains/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataConfiguration.kt diff --git a/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt b/domains/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt similarity index 100% rename from services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt rename to domains/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt diff --git a/services/masterdata/masterdata-service/src/main/resources/db/migration/V001__Create_Land_Table.sql b/domains/masterdata/masterdata-service/src/main/resources/db/migration/V001__Create_Land_Table.sql similarity index 100% rename from services/masterdata/masterdata-service/src/main/resources/db/migration/V001__Create_Land_Table.sql rename to domains/masterdata/masterdata-service/src/main/resources/db/migration/V001__Create_Land_Table.sql diff --git a/services/masterdata/masterdata-service/src/main/resources/db/migration/V002__Create_Bundesland_Table.sql b/domains/masterdata/masterdata-service/src/main/resources/db/migration/V002__Create_Bundesland_Table.sql similarity index 100% rename from services/masterdata/masterdata-service/src/main/resources/db/migration/V002__Create_Bundesland_Table.sql rename to domains/masterdata/masterdata-service/src/main/resources/db/migration/V002__Create_Bundesland_Table.sql diff --git a/services/masterdata/masterdata-service/src/main/resources/db/migration/V003__Create_Altersklasse_Table.sql b/domains/masterdata/masterdata-service/src/main/resources/db/migration/V003__Create_Altersklasse_Table.sql similarity index 100% rename from services/masterdata/masterdata-service/src/main/resources/db/migration/V003__Create_Altersklasse_Table.sql rename to domains/masterdata/masterdata-service/src/main/resources/db/migration/V003__Create_Altersklasse_Table.sql diff --git a/services/masterdata/masterdata-service/src/main/resources/db/migration/V004__Create_Platz_Table.sql b/domains/masterdata/masterdata-service/src/main/resources/db/migration/V004__Create_Platz_Table.sql similarity index 100% rename from services/masterdata/masterdata-service/src/main/resources/db/migration/V004__Create_Platz_Table.sql rename to domains/masterdata/masterdata-service/src/main/resources/db/migration/V004__Create_Platz_Table.sql diff --git a/services/masterdata/masterdata-service/src/main/resources/db/migration/V1__Create_Initial_Tables.sql b/domains/masterdata/masterdata-service/src/main/resources/db/migration/V1__Create_Initial_Tables.sql similarity index 100% rename from services/masterdata/masterdata-service/src/main/resources/db/migration/V1__Create_Initial_Tables.sql rename to domains/masterdata/masterdata-service/src/main/resources/db/migration/V1__Create_Initial_Tables.sql diff --git a/services/masterdata/masterdata-service/src/test/resources/logback-test.xml b/domains/masterdata/masterdata-service/src/test/resources/logback-test.xml similarity index 100% rename from services/masterdata/masterdata-service/src/test/resources/logback-test.xml rename to domains/masterdata/masterdata-service/src/test/resources/logback-test.xml diff --git a/domains/registry/oeps-importer/build.gradle.kts b/domains/registry/oeps-importer/build.gradle.kts new file mode 100644 index 00000000..dd9bee82 --- /dev/null +++ b/domains/registry/oeps-importer/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("java") +} + +group = "at.mocode" +version = "1.0.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + useJUnitPlatform() +} diff --git a/domains/registry/registry-api/build.gradle.kts b/domains/registry/registry-api/build.gradle.kts new file mode 100644 index 00000000..dd9bee82 --- /dev/null +++ b/domains/registry/registry-api/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("java") +} + +group = "at.mocode" +version = "1.0.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + useJUnitPlatform() +} diff --git a/domains/registry/registry-domain/build.gradle.kts b/domains/registry/registry-domain/build.gradle.kts new file mode 100644 index 00000000..dd9bee82 --- /dev/null +++ b/domains/registry/registry-domain/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("java") +} + +group = "at.mocode" +version = "1.0.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + useJUnitPlatform() +} diff --git a/domains/registry/registry-service/build.gradle.kts b/domains/registry/registry-service/build.gradle.kts new file mode 100644 index 00000000..dd9bee82 --- /dev/null +++ b/domains/registry/registry-service/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("java") +} + +group = "at.mocode" +version = "1.0.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + useJUnitPlatform() +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 270e2654..7f41178e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,10 @@ springdoc = "2.8.13" # --- Ktor (API Layer & Client) --- ktor = "3.3.1" +# --- DI --- +koin = "4.0.0" +koinCompose = "4.0.0" + # --- Compose UI --- androidx-lifecycle = "2.9.4" composeHotReload = "1.0.0-rc02" @@ -109,6 +113,11 @@ ktor-server-callLogging = { module = "io.ktor:ktor-server-call-logging-jvm", ver ktor-server-defaultHeaders = { module = "io.ktor:ktor-server-default-headers-jvm", version.ref = "ktor" } ktor-server-rateLimit = { module = "io.ktor:ktor-server-rate-limit-jvm", version.ref = "ktor" } ktor-server-metrics-micrometer = { module = "io.ktor:ktor-server-metrics-micrometer-jvm", version.ref = "ktor" } + +# --- Koin (DI) --- +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinCompose" } +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koinCompose" } ktor-server-openapi = { module = "io.ktor:ktor-server-openapi", version.ref = "ktor" } ktor-server-swagger = { module = "io.ktor:ktor-server-swagger", version.ref = "ktor" } ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "ktor" } diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index df6871aa..d80395ee 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2612,7 +2612,7 @@ source-map-loader@5.0.0: iconv-lite "^0.6.3" source-map-js "^1.0.2" -source-map-support@0.5.21, source-map-support@~0.5.20: +source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== diff --git a/logs/gateway.log b/logs/gateway.log index b1b05910..25bef396 100644 --- a/logs/gateway.log +++ b/logs/gateway.log @@ -1,1696 +1,82 @@ -2025-11-24 16:51:18.531 [background-preinit] INFO [] o.h.validator.internal.util.Version - HV000001: Hibernate Validator 8.0.3.Final -2025-11-24 16:51:18.572 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - Starting GatewayApplicationKt using Java 21.0.9 with PID 40867 (/home/stefan-mo/WsMeldestelle/Meldestelle/infrastructure/gateway/build/classes/kotlin/main started by stefan-mo in /home/stefan-mo/WsMeldestelle/Meldestelle) -2025-11-24 16:51:18.573 [main] DEBUG [] a.m.i.gateway.GatewayApplicationKt - Running with Spring Boot v3.5.6, Spring v6.2.11 -2025-11-24 16:51:18.573 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - The following 1 profile is active: "dev" -2025-11-24 16:51:19.852 [main] INFO [] o.s.cloud.context.scope.GenericScope - BeanFactory id=4eb90187-1826-32ce-9dc7-fa80cb000915 -2025-11-24 16:51:21.685 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [After] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Before] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Between] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Cookie] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Header] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Host] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Method] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Path] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Query] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [ReadBody] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [RemoteAddr] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [XForwardedRemoteAddr] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Weight] -2025-11-24 16:51:21.686 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [CloudFoundryRouteService] -2025-11-24 16:51:22.414 [main] INFO [] o.s.b.a.e.web.EndpointLinksResolver - Exposing 6 endpoints beneath base path '/actuator' -2025-11-24 16:51:23.019 [main] WARN [] o.s.c.l.c.LoadBalancerCacheAutoConfiguration$LoadBalancerCaffeineWarnLogger - Spring Cloud LoadBalancer is currently working with the default cache. While this cache implementation is useful for development and tests, it's recommended to use Caffeine cache in production.You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath. -2025-11-24 16:51:23.095 [catalogWatchTaskScheduler-1] ERROR [] o.s.c.c.discovery.ConsulCatalogWatch - Error watching Consul CatalogServices -com.ecwid.consul.transport.TransportException: org.apache.http.conn.HttpHostConnectException: Connect to localhost:8500 [localhost/127.0.0.1] failed: Verbindungsaufbau abgelehnt - at com.ecwid.consul.transport.AbstractHttpTransport.executeRequest(AbstractHttpTransport.java:83) - at com.ecwid.consul.transport.AbstractHttpTransport.makeGetRequest(AbstractHttpTransport.java:36) - at com.ecwid.consul.v1.ConsulRawClient.makeGetRequest(ConsulRawClient.java:139) - at com.ecwid.consul.v1.catalog.CatalogConsulClient.getCatalogServices(CatalogConsulClient.java:143) - at com.ecwid.consul.v1.ConsulClient.getCatalogServices(ConsulClient.java:400) - at org.springframework.cloud.consul.discovery.ConsulCatalogWatch.catalogServicesWatch(ConsulCatalogWatch.java:131) - at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) - at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358) - at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) - at java.base/java.lang.Thread.run(Thread.java:1583) -Caused by: org.apache.http.conn.HttpHostConnectException: Connect to localhost:8500 [localhost/127.0.0.1] failed: Verbindungsaufbau abgelehnt - at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:156) - at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376) - at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393) - at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236) - at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) - at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) - at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) - at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:72) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:221) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:165) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:140) - at com.ecwid.consul.transport.AbstractHttpTransport.executeRequest(AbstractHttpTransport.java:70) - ... 12 common frames omitted -Caused by: java.net.ConnectException: Verbindungsaufbau abgelehnt - at java.base/sun.nio.ch.Net.pollConnect(Native Method) - at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:682) - at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:542) - at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:592) - at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327) - at java.base/java.net.Socket.connect(Socket.java:751) - at org.apache.http.conn.socket.PlainConnectionSocketFactory.connectSocket(PlainConnectionSocketFactory.java:75) - at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142) - ... 24 common frames omitted -2025-11-24 16:51:23.108 [main] INFO [] o.s.b.w.e.netty.NettyWebServer - Netty started on port 8080 (http) -2025-11-24 16:51:23.111 [main] INFO [] o.s.c.c.s.ConsulServiceRegistry - Registering service with consul: NewService{id='meldestelle-8080-c8ed7bbb-f27c-4551-89ae-2a03cef0ec8c', name='meldestelle', tags=[], address='10.0.0.18', meta={secure=false}, port=8080, enableTagOverride=null, check=Check{script='null', dockerContainerID='null', shell='null', interval='10s', ttl='null', http='http://10.0.0.18:8080/actuator/health', method='null', header={}, tcp='null', timeout='null', deregisterCriticalServiceAfter='null', tlsSkipVerify=null, status='null', grpc='null', grpcUseTLS=null}, checks=null} -2025-11-24 16:51:23.118 [main] ERROR [] o.s.c.c.s.ConsulServiceRegistry - Error registering service with consul: NewService{id='meldestelle-8080-c8ed7bbb-f27c-4551-89ae-2a03cef0ec8c', name='meldestelle', tags=[], address='10.0.0.18', meta={secure=false}, port=8080, enableTagOverride=null, check=Check{script='null', dockerContainerID='null', shell='null', interval='10s', ttl='null', http='http://10.0.0.18:8080/actuator/health', method='null', header={}, tcp='null', timeout='null', deregisterCriticalServiceAfter='null', tlsSkipVerify=null, status='null', grpc='null', grpcUseTLS=null}, checks=null} -com.ecwid.consul.transport.TransportException: org.apache.http.conn.HttpHostConnectException: Connect to localhost:8500 [localhost/127.0.0.1] failed: Verbindungsaufbau abgelehnt - at com.ecwid.consul.transport.AbstractHttpTransport.executeRequest(AbstractHttpTransport.java:83) - at com.ecwid.consul.transport.AbstractHttpTransport.makePutRequest(AbstractHttpTransport.java:49) - at com.ecwid.consul.v1.ConsulRawClient.makePutRequest(ConsulRawClient.java:163) - at com.ecwid.consul.v1.agent.AgentConsulClient.agentServiceRegister(AgentConsulClient.java:273) - at com.ecwid.consul.v1.ConsulClient.agentServiceRegister(ConsulClient.java:310) - at org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry.register(ConsulServiceRegistry.java:67) - at org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry.register(ConsulServiceRegistry.java:43) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.register(AbstractAutoServiceRegistration.java:264) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.register(ConsulAutoServiceRegistration.java:80) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.start(AbstractAutoServiceRegistration.java:156) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.start(ConsulAutoServiceRegistration.java:70) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationListener.onApplicationEvent(ConsulAutoServiceRegistrationListener.java:60) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.boot.web.reactive.context.WebServerManager.start(WebServerManager.java:57) - at org.springframework.boot.web.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:405) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:394) - at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:586) - at java.base/java.lang.Iterable.forEach(Iterable.java:75) - at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:364) - at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:310) - at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1006) - at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:630) - at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) - at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) - at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) - at at.mocode.infrastructure.gateway.GatewayApplicationKt.main(GatewayApplication.kt:13) -Caused by: org.apache.http.conn.HttpHostConnectException: Connect to localhost:8500 [localhost/127.0.0.1] failed: Verbindungsaufbau abgelehnt - at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:156) - at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376) - at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393) - at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236) - at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) - at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) - at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) - at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:72) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:221) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:165) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:140) - at com.ecwid.consul.transport.AbstractHttpTransport.executeRequest(AbstractHttpTransport.java:70) - ... 33 common frames omitted -Caused by: java.net.ConnectException: Verbindungsaufbau abgelehnt - at java.base/sun.nio.ch.Net.pollConnect(Native Method) - at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:682) - at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:542) - at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:592) - at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327) - at java.base/java.net.Socket.connect(Socket.java:751) - at org.apache.http.conn.socket.PlainConnectionSocketFactory.connectSocket(PlainConnectionSocketFactory.java:75) - at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142) - ... 45 common frames omitted -2025-11-24 16:51:25.129 [main] WARN [] o.s.b.w.r.c.AnnotationConfigReactiveWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop' -2025-11-24 16:51:25.154 [main] INFO [] o.s.b.a.l.ConditionEvaluationReportLogger - +2025-11-27 19:01:19.988 [background-preinit] INFO [] o.h.validator.internal.util.Version - HV000001: Hibernate Validator 8.0.3.Final +2025-11-27 19:01:20.022 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - Starting GatewayApplicationKt using Java 21.0.9 with PID 56550 (/home/stefan-mo/WsMeldestelle/Meldestelle/infrastructure/gateway/build/classes/kotlin/main started by stefan-mo in /home/stefan-mo/WsMeldestelle/Meldestelle) +2025-11-27 19:01:20.022 [main] DEBUG [] a.m.i.gateway.GatewayApplicationKt - Running with Spring Boot v3.5.6, Spring v6.2.11 +2025-11-27 19:01:20.022 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - The following 1 profile is active: "dev" +2025-11-27 19:01:21.201 [main] INFO [] o.s.cloud.context.scope.GenericScope - BeanFactory id=4eb90187-1826-32ce-9dc7-fa80cb000915 +2025-11-27 19:01:23.095 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [After] +2025-11-27 19:01:23.095 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Before] +2025-11-27 19:01:23.095 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Between] +2025-11-27 19:01:23.095 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Cookie] +2025-11-27 19:01:23.095 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Header] +2025-11-27 19:01:23.095 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Host] +2025-11-27 19:01:23.096 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Method] +2025-11-27 19:01:23.096 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Path] +2025-11-27 19:01:23.096 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Query] +2025-11-27 19:01:23.096 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [ReadBody] +2025-11-27 19:01:23.096 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [RemoteAddr] +2025-11-27 19:01:23.096 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [XForwardedRemoteAddr] +2025-11-27 19:01:23.096 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Weight] +2025-11-27 19:01:23.096 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [CloudFoundryRouteService] +2025-11-27 19:01:23.674 [main] INFO [] o.s.b.a.e.web.EndpointLinksResolver - Exposing 6 endpoints beneath base path '/actuator' +2025-11-27 19:01:24.297 [main] WARN [] o.s.c.l.c.LoadBalancerCacheAutoConfiguration$LoadBalancerCaffeineWarnLogger - Spring Cloud LoadBalancer is currently working with the default cache. While this cache implementation is useful for development and tests, it's recommended to use Caffeine cache in production.You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath. +2025-11-27 19:01:24.430 [main] INFO [] o.s.b.w.e.netty.NettyWebServer - Netty started on port 8080 (http) +2025-11-27 19:01:24.434 [main] INFO [] o.s.c.c.s.ConsulServiceRegistry - Registering service with consul: NewService{id='meldestelle-8080-74499425-14ca-46e0-9d2c-2a31faa28455', name='meldestelle', tags=[], address='10.0.0.18', meta={secure=false}, port=8080, enableTagOverride=null, check=Check{script='null', dockerContainerID='null', shell='null', interval='10s', ttl='null', http='http://10.0.0.18:8080/actuator/health', method='null', header={}, tcp='null', timeout='null', deregisterCriticalServiceAfter='null', tlsSkipVerify=null, status='null', grpc='null', grpcUseTLS=null}, checks=null} +2025-11-27 19:01:24.559 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - Started GatewayApplicationKt in 4.894 seconds (process running for 5.462) +2025-11-27 19:03:18.798 [SpringApplicationShutdownHook] INFO [] o.s.b.w.e.netty.GracefulShutdown - Commencing graceful shutdown. Waiting for active requests to complete +2025-11-27 19:03:18.799 [netty-shutdown] INFO [] o.s.b.w.e.netty.GracefulShutdown - Graceful shutdown complete +2025-11-27 19:03:20.810 [SpringApplicationShutdownHook] INFO [] o.s.c.c.s.ConsulServiceRegistry - Deregistering service with consul: meldestelle-8080-74499425-14ca-46e0-9d2c-2a31faa28455 +2025-11-27 19:06:42.612 [background-preinit] INFO [] o.h.validator.internal.util.Version - HV000001: Hibernate Validator 8.0.3.Final +2025-11-27 19:06:42.637 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - Starting GatewayApplicationKt using Java 21.0.9 with PID 61912 (/home/stefan-mo/WsMeldestelle/Meldestelle/infrastructure/gateway/build/classes/kotlin/main started by stefan-mo in /home/stefan-mo/WsMeldestelle/Meldestelle) +2025-11-27 19:06:42.638 [main] DEBUG [] a.m.i.gateway.GatewayApplicationKt - Running with Spring Boot v3.5.6, Spring v6.2.11 +2025-11-27 19:06:42.638 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - The following 1 profile is active: "dev" +2025-11-27 19:06:43.726 [main] INFO [] o.s.cloud.context.scope.GenericScope - BeanFactory id=4eb90187-1826-32ce-9dc7-fa80cb000915 +2025-11-27 19:06:44.156 [main] WARN [] o.s.b.w.r.c.AnnotationConfigReactiveWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'gatewayHealthIndicator' defined in file [/home/stefan-mo/WsMeldestelle/Meldestelle/infrastructure/gateway/build/classes/kotlin/main/at/mocode/infrastructure/gateway/health/GatewayHealthIndicator.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'compositeDiscoveryClient' defined in class path resource [org/springframework/cloud/client/discovery/composite/CompositeDiscoveryClientAutoConfiguration.class]: Unsatisfied dependency expressed through method 'compositeDiscoveryClient' parameter 0: Error creating bean with name 'consulDiscoveryClient' defined in class path resource [org/springframework/cloud/consul/discovery/ConsulDiscoveryClientConfiguration.class]: Unsatisfied dependency expressed through method 'consulDiscoveryClient' parameter 0: Error creating bean with name 'consulClient' defined in class path resource [org/springframework/cloud/consul/ConsulAutoConfiguration.class]: Unsatisfied dependency expressed through method 'consulClient' parameter 0: Error creating bean with name 'consulProperties': Could not bind properties to 'ConsulProperties' : prefix=spring.cloud.consul, ignoreInvalidFields=false, ignoreUnknownFields=true +2025-11-27 19:06:44.169 [main] INFO [] o.s.b.a.l.ConditionEvaluationReportLogger - Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. -2025-11-24 16:51:25.167 [main] ERROR [] o.s.boot.SpringApplication - Application run failed -org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop' - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:408) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:394) - at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:586) - at java.base/java.lang.Iterable.forEach(Iterable.java:75) - at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:364) - at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:310) - at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1006) - at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:630) - at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) - at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) - at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) - at at.mocode.infrastructure.gateway.GatewayApplicationKt.main(GatewayApplication.kt:13) -Caused by: com.ecwid.consul.transport.TransportException: org.apache.http.conn.HttpHostConnectException: Connect to localhost:8500 [localhost/127.0.0.1] failed: Verbindungsaufbau abgelehnt - at com.ecwid.consul.transport.AbstractHttpTransport.executeRequest(AbstractHttpTransport.java:83) - at com.ecwid.consul.transport.AbstractHttpTransport.makePutRequest(AbstractHttpTransport.java:49) - at com.ecwid.consul.v1.ConsulRawClient.makePutRequest(ConsulRawClient.java:163) - at com.ecwid.consul.v1.agent.AgentConsulClient.agentServiceRegister(AgentConsulClient.java:273) - at com.ecwid.consul.v1.ConsulClient.agentServiceRegister(ConsulClient.java:310) - at org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry.register(ConsulServiceRegistry.java:67) - at org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry.register(ConsulServiceRegistry.java:43) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.register(AbstractAutoServiceRegistration.java:264) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.register(ConsulAutoServiceRegistration.java:80) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.start(AbstractAutoServiceRegistration.java:156) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.start(ConsulAutoServiceRegistration.java:70) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationListener.onApplicationEvent(ConsulAutoServiceRegistrationListener.java:60) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.boot.web.reactive.context.WebServerManager.start(WebServerManager.java:57) - at org.springframework.boot.web.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:405) - ... 14 common frames omitted -Caused by: org.apache.http.conn.HttpHostConnectException: Connect to localhost:8500 [localhost/127.0.0.1] failed: Verbindungsaufbau abgelehnt - at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:156) - at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376) - at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393) - at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236) - at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) - at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) - at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) - at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:72) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:221) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:165) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:140) - at com.ecwid.consul.transport.AbstractHttpTransport.executeRequest(AbstractHttpTransport.java:70) - ... 33 common frames omitted -Caused by: java.net.ConnectException: Verbindungsaufbau abgelehnt - at java.base/sun.nio.ch.Net.pollConnect(Native Method) - at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:682) - at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:542) - at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:592) - at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327) - at java.base/java.net.Socket.connect(Socket.java:751) - at org.apache.http.conn.socket.PlainConnectionSocketFactory.connectSocket(PlainConnectionSocketFactory.java:75) - at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142) - ... 45 common frames omitted -2025-11-24 16:53:49.126 [background-preinit] INFO [] o.h.validator.internal.util.Version - HV000001: Hibernate Validator 8.0.3.Final -2025-11-24 16:53:49.150 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - Starting GatewayApplicationKt using Java 21.0.9 with PID 43342 (/home/stefan-mo/WsMeldestelle/Meldestelle/infrastructure/gateway/build/classes/kotlin/main started by stefan-mo in /home/stefan-mo/WsMeldestelle/Meldestelle) -2025-11-24 16:53:49.151 [main] DEBUG [] a.m.i.gateway.GatewayApplicationKt - Running with Spring Boot v3.5.6, Spring v6.2.11 -2025-11-24 16:53:49.151 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - The following 1 profile is active: "dev" -2025-11-24 16:53:50.293 [main] INFO [] o.s.cloud.context.scope.GenericScope - BeanFactory id=4eb90187-1826-32ce-9dc7-fa80cb000915 -2025-11-24 16:53:52.218 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [After] -2025-11-24 16:53:52.218 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Before] -2025-11-24 16:53:52.218 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Between] -2025-11-24 16:53:52.218 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Cookie] -2025-11-24 16:53:52.218 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Header] -2025-11-24 16:53:52.219 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Host] -2025-11-24 16:53:52.219 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Method] -2025-11-24 16:53:52.219 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Path] -2025-11-24 16:53:52.219 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Query] -2025-11-24 16:53:52.219 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [ReadBody] -2025-11-24 16:53:52.219 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [RemoteAddr] -2025-11-24 16:53:52.219 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [XForwardedRemoteAddr] -2025-11-24 16:53:52.219 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Weight] -2025-11-24 16:53:52.219 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [CloudFoundryRouteService] -2025-11-24 16:53:52.877 [main] INFO [] o.s.b.a.e.web.EndpointLinksResolver - Exposing 6 endpoints beneath base path '/actuator' -2025-11-24 16:53:53.453 [main] WARN [] o.s.c.l.c.LoadBalancerCacheAutoConfiguration$LoadBalancerCaffeineWarnLogger - Spring Cloud LoadBalancer is currently working with the default cache. While this cache implementation is useful for development and tests, it's recommended to use Caffeine cache in production.You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath. -2025-11-24 16:53:53.546 [main] INFO [] o.s.b.w.e.netty.NettyWebServer - Netty started on port 8080 (http) -2025-11-24 16:53:53.548 [main] INFO [] o.s.c.c.s.ConsulServiceRegistry - Registering service with consul: NewService{id='meldestelle-8080-aa508881-4ee2-415a-8b2c-f9168af38813', name='meldestelle', tags=[], address='10.0.0.18', meta={secure=false}, port=8080, enableTagOverride=null, check=Check{script='null', dockerContainerID='null', shell='null', interval='10s', ttl='null', http='http://10.0.0.18:8080/actuator/health', method='null', header={}, tcp='null', timeout='null', deregisterCriticalServiceAfter='null', tlsSkipVerify=null, status='null', grpc='null', grpcUseTLS=null}, checks=null} -2025-11-24 16:53:53.598 [catalogWatchTaskScheduler-1] ERROR [] o.s.c.g.route.CachingRouteLocator - Refresh routes error !!! -java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.resetIfNeeded(RouteRefreshListener.java:70) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:64) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.consul.discovery.ConsulCatalogWatch.catalogServicesWatch(ConsulCatalogWatch.java:140) - at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) - at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358) - at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) - at java.base/java.lang.Thread.run(Thread.java:1583) -2025-11-24 16:53:53.598 [main] ERROR [] o.s.c.g.route.CachingRouteLocator - Refresh routes error !!! -java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:58) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.start(AbstractAutoServiceRegistration.java:169) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.start(ConsulAutoServiceRegistration.java:70) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationListener.onApplicationEvent(ConsulAutoServiceRegistrationListener.java:60) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.boot.web.reactive.context.WebServerManager.start(WebServerManager.java:57) - at org.springframework.boot.web.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:405) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:394) - at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:586) - at java.base/java.lang.Iterable.forEach(Iterable.java:75) - at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:364) - at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:310) - at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1006) - at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:630) - at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) - at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) - at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) - at at.mocode.infrastructure.gateway.GatewayApplicationKt.main(GatewayApplication.kt:13) -2025-11-24 16:53:53.606 [catalogWatchTaskScheduler-1] ERROR [] reactor.core.publisher.Operators - Operator called default onErrorDropped -reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus -Caused by: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68) - at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4425) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:65) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:44) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.CachingRouteLocator.handleRefreshError(CachingRouteLocator.java:126) - at reactor.core.publisher.LambdaMonoSubscriber.doError(LambdaMonoSubscriber.java:155) - at reactor.core.publisher.LambdaMonoSubscriber.onError(LambdaMonoSubscriber.java:150) - at reactor.core.publisher.MonoStreamCollector$StreamCollectorSubscriber.onError(MonoStreamCollector.java:149) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:351) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onError(FluxFlattenIterable.java:263) - at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onError(FluxPeekFuseable.java:234) - at reactor.core.publisher.MonoCollectList$MonoCollectListSubscriber.onError(MonoCollectList.java:108) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:359) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerError(FluxMergeSequential.java:321) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onError(FluxMergeSequential.java:586) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.resetIfNeeded(RouteRefreshListener.java:70) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:64) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.consul.discovery.ConsulCatalogWatch.catalogServicesWatch(ConsulCatalogWatch.java:140) - at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) - at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358) - at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) - at java.base/java.lang.Thread.run(Thread.java:1583) -2025-11-24 16:53:53.607 [main] ERROR [] reactor.core.publisher.Operators - Operator called default onErrorDropped -reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus -Caused by: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68) - at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4425) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:65) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:44) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.CachingRouteLocator.handleRefreshError(CachingRouteLocator.java:126) - at reactor.core.publisher.LambdaMonoSubscriber.doError(LambdaMonoSubscriber.java:155) - at reactor.core.publisher.LambdaMonoSubscriber.onError(LambdaMonoSubscriber.java:150) - at reactor.core.publisher.MonoStreamCollector$StreamCollectorSubscriber.onError(MonoStreamCollector.java:149) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:351) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onError(FluxFlattenIterable.java:263) - at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onError(FluxPeekFuseable.java:234) - at reactor.core.publisher.MonoCollectList$MonoCollectListSubscriber.onError(MonoCollectList.java:108) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:359) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerError(FluxMergeSequential.java:321) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onError(FluxMergeSequential.java:586) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:58) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.start(AbstractAutoServiceRegistration.java:169) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.start(ConsulAutoServiceRegistration.java:70) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationListener.onApplicationEvent(ConsulAutoServiceRegistrationListener.java:60) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.boot.web.reactive.context.WebServerManager.start(WebServerManager.java:57) - at org.springframework.boot.web.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:405) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:394) - at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:586) - at java.base/java.lang.Iterable.forEach(Iterable.java:75) - at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:364) - at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:310) - at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1006) - at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:630) - at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) - at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) - at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) - at at.mocode.infrastructure.gateway.GatewayApplicationKt.main(GatewayApplication.kt:13) -2025-11-24 16:53:53.608 [catalogWatchTaskScheduler-1] ERROR [] o.s.c.c.discovery.ConsulCatalogWatch - Error watching Consul CatalogServices -java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68) - at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4425) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:65) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:44) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.CachingRouteLocator.handleRefreshError(CachingRouteLocator.java:126) - at reactor.core.publisher.LambdaMonoSubscriber.doError(LambdaMonoSubscriber.java:155) - at reactor.core.publisher.LambdaMonoSubscriber.onError(LambdaMonoSubscriber.java:150) - at reactor.core.publisher.MonoStreamCollector$StreamCollectorSubscriber.onError(MonoStreamCollector.java:149) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:351) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onError(FluxFlattenIterable.java:263) - at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onError(FluxPeekFuseable.java:234) - at reactor.core.publisher.MonoCollectList$MonoCollectListSubscriber.onError(MonoCollectList.java:108) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:359) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerError(FluxMergeSequential.java:321) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onError(FluxMergeSequential.java:586) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:58) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.start(AbstractAutoServiceRegistration.java:169) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.start(ConsulAutoServiceRegistration.java:70) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationListener.onApplicationEvent(ConsulAutoServiceRegistrationListener.java:60) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.boot.web.reactive.context.WebServerManager.start(WebServerManager.java:57) - at org.springframework.boot.web.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:405) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:394) - at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:586) - at java.base/java.lang.Iterable.forEach(Iterable.java:75) - at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:364) - at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:310) - at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1006) - at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:630) - at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) - at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) - at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) - at at.mocode.infrastructure.gateway.GatewayApplicationKt.main(GatewayApplication.kt:13) - Suppressed: java.lang.Exception: #block terminated with an error - at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:104) - at reactor.core.publisher.Flux.blockLast(Flux.java:2817) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.lambda$onApplicationEvent$3(WeightCalculatorWebFilter.java:156) - at org.springframework.beans.factory.support.DefaultListableBeanFactory$DependencyObjectProvider.ifAvailable(DefaultListableBeanFactory.java:2562) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.onApplicationEvent(WeightCalculatorWebFilter.java:156) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.resetIfNeeded(RouteRefreshListener.java:70) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:64) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.consul.discovery.ConsulCatalogWatch.catalogServicesWatch(ConsulCatalogWatch.java:140) - at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) - at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358) - at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) - at java.base/java.lang.Thread.run(Thread.java:1583) -2025-11-24 16:53:53.608 [main] ERROR [] reactor.core.publisher.Operators - Operator called default onErrorDropped -reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus -Caused by: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68) - at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4425) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:65) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:44) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.CachingRouteLocator.handleRefreshError(CachingRouteLocator.java:126) - at reactor.core.publisher.LambdaMonoSubscriber.doError(LambdaMonoSubscriber.java:155) - at reactor.core.publisher.LambdaMonoSubscriber.onError(LambdaMonoSubscriber.java:150) - at reactor.core.publisher.MonoStreamCollector$StreamCollectorSubscriber.onError(MonoStreamCollector.java:149) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:351) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onError(FluxFlattenIterable.java:263) - at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onError(FluxPeekFuseable.java:234) - at reactor.core.publisher.MonoCollectList$MonoCollectListSubscriber.onError(MonoCollectList.java:108) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:359) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerError(FluxMergeSequential.java:321) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onError(FluxMergeSequential.java:586) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:58) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.start(AbstractAutoServiceRegistration.java:169) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.start(ConsulAutoServiceRegistration.java:70) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationListener.onApplicationEvent(ConsulAutoServiceRegistrationListener.java:60) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.boot.web.reactive.context.WebServerManager.start(WebServerManager.java:57) - at org.springframework.boot.web.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:405) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:394) - at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:586) - at java.base/java.lang.Iterable.forEach(Iterable.java:75) - at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:364) - at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:310) - at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1006) - at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:630) - at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) - at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) - at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) - at at.mocode.infrastructure.gateway.GatewayApplicationKt.main(GatewayApplication.kt:13) - Suppressed: java.lang.Exception: #block terminated with an error - at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:104) - at reactor.core.publisher.Flux.blockLast(Flux.java:2817) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.lambda$onApplicationEvent$3(WeightCalculatorWebFilter.java:156) - at org.springframework.beans.factory.support.DefaultListableBeanFactory$DependencyObjectProvider.ifAvailable(DefaultListableBeanFactory.java:2562) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.onApplicationEvent(WeightCalculatorWebFilter.java:156) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.resetIfNeeded(RouteRefreshListener.java:70) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:64) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.consul.discovery.ConsulCatalogWatch.catalogServicesWatch(ConsulCatalogWatch.java:140) - at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) - at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358) - at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) - at java.base/java.lang.Thread.run(Thread.java:1583) -2025-11-24 16:53:53.640 [main] ERROR [] o.s.c.g.route.CachingRouteLocator - Refresh routes error !!! -java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:54) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1009) - at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:630) - at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) - at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) - at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) - at at.mocode.infrastructure.gateway.GatewayApplicationKt.main(GatewayApplication.kt:13) -2025-11-24 16:53:53.640 [main] ERROR [] reactor.core.publisher.Operators - Operator called default onErrorDropped -reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus -Caused by: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68) - at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4425) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:65) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:44) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.CachingRouteLocator.handleRefreshError(CachingRouteLocator.java:126) - at reactor.core.publisher.LambdaMonoSubscriber.doError(LambdaMonoSubscriber.java:155) - at reactor.core.publisher.LambdaMonoSubscriber.onError(LambdaMonoSubscriber.java:150) - at reactor.core.publisher.MonoStreamCollector$StreamCollectorSubscriber.onError(MonoStreamCollector.java:149) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:351) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onError(FluxFlattenIterable.java:263) - at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onError(FluxPeekFuseable.java:234) - at reactor.core.publisher.MonoCollectList$MonoCollectListSubscriber.onError(MonoCollectList.java:108) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:359) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerError(FluxMergeSequential.java:321) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onError(FluxMergeSequential.java:586) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:58) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.start(AbstractAutoServiceRegistration.java:169) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.start(ConsulAutoServiceRegistration.java:70) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationListener.onApplicationEvent(ConsulAutoServiceRegistrationListener.java:60) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.boot.web.reactive.context.WebServerManager.start(WebServerManager.java:57) - at org.springframework.boot.web.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:405) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:394) - at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:586) - at java.base/java.lang.Iterable.forEach(Iterable.java:75) - at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:364) - at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:310) - at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1006) - at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:630) - at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) - at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) - at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) - at at.mocode.infrastructure.gateway.GatewayApplicationKt.main(GatewayApplication.kt:13) - Suppressed: java.lang.Exception: #block terminated with an error - at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:104) - at reactor.core.publisher.Flux.blockLast(Flux.java:2817) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.lambda$onApplicationEvent$3(WeightCalculatorWebFilter.java:156) - at org.springframework.beans.factory.support.DefaultListableBeanFactory$DependencyObjectProvider.ifAvailable(DefaultListableBeanFactory.java:2562) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.onApplicationEvent(WeightCalculatorWebFilter.java:156) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.resetIfNeeded(RouteRefreshListener.java:70) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:64) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.consul.discovery.ConsulCatalogWatch.catalogServicesWatch(ConsulCatalogWatch.java:140) - at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) - at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358) - at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) - at java.base/java.lang.Thread.run(Thread.java:1583) -2025-11-24 16:53:53.640 [main] ERROR [] reactor.core.publisher.Operators - Operator called default onErrorDropped -reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus -Caused by: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68) - at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4425) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:65) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:44) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.CachingRouteLocator.handleRefreshError(CachingRouteLocator.java:126) - at reactor.core.publisher.LambdaMonoSubscriber.doError(LambdaMonoSubscriber.java:155) - at reactor.core.publisher.LambdaMonoSubscriber.onError(LambdaMonoSubscriber.java:150) - at reactor.core.publisher.MonoStreamCollector$StreamCollectorSubscriber.onError(MonoStreamCollector.java:149) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:351) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onError(FluxFlattenIterable.java:263) - at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onError(FluxPeekFuseable.java:234) - at reactor.core.publisher.MonoCollectList$MonoCollectListSubscriber.onError(MonoCollectList.java:108) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:359) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerError(FluxMergeSequential.java:321) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onError(FluxMergeSequential.java:586) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:58) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.start(AbstractAutoServiceRegistration.java:169) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.start(ConsulAutoServiceRegistration.java:70) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationListener.onApplicationEvent(ConsulAutoServiceRegistrationListener.java:60) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.boot.web.reactive.context.WebServerManager.start(WebServerManager.java:57) - at org.springframework.boot.web.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:405) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:394) - at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:586) - at java.base/java.lang.Iterable.forEach(Iterable.java:75) - at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:364) - at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:310) - at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1006) - at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:630) - at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) - at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) - at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) - at at.mocode.infrastructure.gateway.GatewayApplicationKt.main(GatewayApplication.kt:13) - Suppressed: java.lang.Exception: #block terminated with an error - at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:104) - at reactor.core.publisher.Flux.blockLast(Flux.java:2817) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.lambda$onApplicationEvent$3(WeightCalculatorWebFilter.java:156) - at org.springframework.beans.factory.support.DefaultListableBeanFactory$DependencyObjectProvider.ifAvailable(DefaultListableBeanFactory.java:2562) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.onApplicationEvent(WeightCalculatorWebFilter.java:156) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.resetIfNeeded(RouteRefreshListener.java:70) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:64) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.consul.discovery.ConsulCatalogWatch.catalogServicesWatch(ConsulCatalogWatch.java:140) - at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) - at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358) - at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) - at java.base/java.lang.Thread.run(Thread.java:1583) -2025-11-24 16:53:53.643 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - Started GatewayApplicationKt in 4.812 seconds (process running for 5.333) -2025-11-24 16:53:54.614 [catalogWatchTaskScheduler-1] ERROR [] o.s.c.g.route.CachingRouteLocator - Refresh routes error !!! -java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.resetIfNeeded(RouteRefreshListener.java:70) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:64) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.consul.discovery.ConsulCatalogWatch.catalogServicesWatch(ConsulCatalogWatch.java:140) - at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) - at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358) - at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) - at java.base/java.lang.Thread.run(Thread.java:1583) -2025-11-24 16:53:54.615 [catalogWatchTaskScheduler-1] ERROR [] reactor.core.publisher.Operators - Operator called default onErrorDropped -reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus -Caused by: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68) - at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4425) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:65) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:44) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.CachingRouteLocator.handleRefreshError(CachingRouteLocator.java:126) - at reactor.core.publisher.LambdaMonoSubscriber.doError(LambdaMonoSubscriber.java:155) - at reactor.core.publisher.LambdaMonoSubscriber.onError(LambdaMonoSubscriber.java:150) - at reactor.core.publisher.MonoStreamCollector$StreamCollectorSubscriber.onError(MonoStreamCollector.java:149) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:351) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onError(FluxFlattenIterable.java:263) - at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onError(FluxPeekFuseable.java:234) - at reactor.core.publisher.MonoCollectList$MonoCollectListSubscriber.onError(MonoCollectList.java:108) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:359) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerError(FluxMergeSequential.java:321) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onError(FluxMergeSequential.java:586) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:58) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.start(AbstractAutoServiceRegistration.java:169) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.start(ConsulAutoServiceRegistration.java:70) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationListener.onApplicationEvent(ConsulAutoServiceRegistrationListener.java:60) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.boot.web.reactive.context.WebServerManager.start(WebServerManager.java:57) - at org.springframework.boot.web.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:405) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:394) - at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:586) - at java.base/java.lang.Iterable.forEach(Iterable.java:75) - at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:364) - at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:310) - at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1006) - at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:630) - at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) - at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) - at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) - at at.mocode.infrastructure.gateway.GatewayApplicationKt.main(GatewayApplication.kt:13) - Suppressed: java.lang.Exception: #block terminated with an error - at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:104) - at reactor.core.publisher.Flux.blockLast(Flux.java:2817) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.lambda$onApplicationEvent$3(WeightCalculatorWebFilter.java:156) - at org.springframework.beans.factory.support.DefaultListableBeanFactory$DependencyObjectProvider.ifAvailable(DefaultListableBeanFactory.java:2562) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.onApplicationEvent(WeightCalculatorWebFilter.java:156) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.resetIfNeeded(RouteRefreshListener.java:70) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:64) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.consul.discovery.ConsulCatalogWatch.catalogServicesWatch(ConsulCatalogWatch.java:140) - at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) - at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358) - at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) - at java.base/java.lang.Thread.run(Thread.java:1583) -2025-11-24 16:53:54.615 [catalogWatchTaskScheduler-1] ERROR [] reactor.core.publisher.Operators - Operator called default onErrorDropped -reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus -Caused by: java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name setStatus - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.loadGatewayFilters(RouteDefinitionRouteLocator.java:145) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.getFilters(RouteDefinitionRouteLocator.java:191) - at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:132) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68) - at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4425) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:65) - at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:44) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.CachingRouteLocator.handleRefreshError(CachingRouteLocator.java:126) - at reactor.core.publisher.LambdaMonoSubscriber.doError(LambdaMonoSubscriber.java:155) - at reactor.core.publisher.LambdaMonoSubscriber.onError(LambdaMonoSubscriber.java:150) - at reactor.core.publisher.MonoStreamCollector$StreamCollectorSubscriber.onError(MonoStreamCollector.java:149) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:351) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724) - at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onError(FluxFlattenIterable.java:263) - at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onError(FluxPeekFuseable.java:234) - at reactor.core.publisher.MonoCollectList$MonoCollectListSubscriber.onError(MonoCollectList.java:108) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:359) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerError(FluxMergeSequential.java:321) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onError(FluxMergeSequential.java:586) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) - at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) - at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.drain(FluxMergeSequential.java:439) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.innerNext(FluxMergeSequential.java:304) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onNext(FluxMergeSequential.java:578) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:148) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.slowPath(FluxIterable.java:664) - at reactor.core.publisher.FluxIterable$IterableSubscriptionConditional.request(FluxIterable.java:623) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialInner.onSubscribe(FluxMergeSequential.java:571) - at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:179) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Flux.subscribe(Flux.java:8891) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:237) - at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) - at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) - at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) - at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) - at reactor.core.publisher.Mono.subscribe(Mono.java:4576) - at reactor.core.publisher.Mono.subscribeWith(Mono.java:4641) - at reactor.core.publisher.Mono.subscribe(Mono.java:4542) - at reactor.core.publisher.Mono.subscribe(Mono.java:4478) - at reactor.core.publisher.Mono.subscribe(Mono.java:4450) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:98) - at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:42) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:58) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration.start(AbstractAutoServiceRegistration.java:169) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistration.start(ConsulAutoServiceRegistration.java:70) - at org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationListener.onApplicationEvent(ConsulAutoServiceRegistrationListener.java:60) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.boot.web.reactive.context.WebServerManager.start(WebServerManager.java:57) - at org.springframework.boot.web.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:405) - at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:394) - at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:586) - at java.base/java.lang.Iterable.forEach(Iterable.java:75) - at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:364) - at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:310) - at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1006) - at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:630) - at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) - at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) - at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) - at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) - at at.mocode.infrastructure.gateway.GatewayApplicationKt.main(GatewayApplication.kt:13) - Suppressed: java.lang.Exception: #block terminated with an error - at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:104) - at reactor.core.publisher.Flux.blockLast(Flux.java:2817) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.lambda$onApplicationEvent$3(WeightCalculatorWebFilter.java:156) - at org.springframework.beans.factory.support.DefaultListableBeanFactory$DependencyObjectProvider.ifAvailable(DefaultListableBeanFactory.java:2562) - at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.onApplicationEvent(WeightCalculatorWebFilter.java:156) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:75) - at org.springframework.cloud.gateway.route.RouteRefreshListener.resetIfNeeded(RouteRefreshListener.java:70) - at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:64) - at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185) - at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178) - at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:454) - at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:387) - at org.springframework.cloud.consul.discovery.ConsulCatalogWatch.catalogServicesWatch(ConsulCatalogWatch.java:140) - at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) - at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358) - at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) - at java.base/java.lang.Thread.run(Thread.java:1583) -2025-11-24 17:28:21.609 [SpringApplicationShutdownHook] INFO [] o.s.b.w.e.netty.GracefulShutdown - Commencing graceful shutdown. Waiting for active requests to complete -2025-11-24 17:28:21.611 [netty-shutdown] INFO [] o.s.b.w.e.netty.GracefulShutdown - Graceful shutdown complete -2025-11-24 17:28:23.623 [SpringApplicationShutdownHook] INFO [] o.s.c.c.s.ConsulServiceRegistry - Deregistering service with consul: meldestelle-8080-aa508881-4ee2-415a-8b2c-f9168af38813 +2025-11-27 19:06:44.181 [main] ERROR [] o.s.b.d.LoggingFailureAnalysisReporter - + +*************************** +APPLICATION FAILED TO START +*************************** + +Description: + +Failed to bind properties under 'spring.cloud.consul.port' to int: + + Property: spring.cloud.consul.port + Value: "${CONSUL_PORT:8500}" + Origin: class path resource [application.yml] - 22:13 + Reason: failed to convert java.lang.String to @jakarta.validation.constraints.NotNull int (caused by java.lang.NumberFormatException: For input string: "8500:8500") + +Action: + +Update your application's configuration + +2025-11-27 19:07:39.516 [background-preinit] INFO [] o.h.validator.internal.util.Version - HV000001: Hibernate Validator 8.0.3.Final +2025-11-27 19:07:39.548 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - Starting GatewayApplicationKt using Java 21.0.9 with PID 62913 (/home/stefan-mo/WsMeldestelle/Meldestelle/infrastructure/gateway/build/classes/kotlin/main started by stefan-mo in /home/stefan-mo/WsMeldestelle/Meldestelle) +2025-11-27 19:07:39.548 [main] DEBUG [] a.m.i.gateway.GatewayApplicationKt - Running with Spring Boot v3.5.6, Spring v6.2.11 +2025-11-27 19:07:39.549 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - The following 1 profile is active: "dev" +2025-11-27 19:07:40.666 [main] INFO [] o.s.cloud.context.scope.GenericScope - BeanFactory id=4eb90187-1826-32ce-9dc7-fa80cb000915 +2025-11-27 19:07:42.677 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [After] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Before] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Between] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Cookie] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Header] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Host] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Method] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Path] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Query] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [ReadBody] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [RemoteAddr] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [XForwardedRemoteAddr] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [Weight] +2025-11-27 19:07:42.678 [main] INFO [] o.s.c.g.r.RouteDefinitionRouteLocator - Loaded RoutePredicateFactory [CloudFoundryRouteService] +2025-11-27 19:07:43.362 [main] INFO [] o.s.b.a.e.web.EndpointLinksResolver - Exposing 6 endpoints beneath base path '/actuator' +2025-11-27 19:07:43.989 [main] WARN [] o.s.c.l.c.LoadBalancerCacheAutoConfiguration$LoadBalancerCaffeineWarnLogger - Spring Cloud LoadBalancer is currently working with the default cache. While this cache implementation is useful for development and tests, it's recommended to use Caffeine cache in production.You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath. +2025-11-27 19:07:44.087 [main] INFO [] o.s.b.w.e.netty.NettyWebServer - Netty started on port 8080 (http) +2025-11-27 19:07:44.090 [main] INFO [] o.s.c.c.s.ConsulServiceRegistry - Registering service with consul: NewService{id='meldestelle-8080-54f57945-6e65-4dd4-bb1d-ddec4966a68a', name='meldestelle', tags=[], address='10.0.0.18', meta={secure=false}, port=8080, enableTagOverride=null, check=Check{script='null', dockerContainerID='null', shell='null', interval='10s', ttl='null', http='http://10.0.0.18:8080/actuator/health', method='null', header={}, tcp='null', timeout='null', deregisterCriticalServiceAfter='null', tlsSkipVerify=null, status='null', grpc='null', grpcUseTLS=null}, checks=null} +2025-11-27 19:07:44.196 [main] INFO [] a.m.i.gateway.GatewayApplicationKt - Started GatewayApplicationKt in 4.979 seconds (process running for 5.524) +2025-11-27 19:07:48.007 [SpringApplicationShutdownHook] INFO [] o.s.b.w.e.netty.GracefulShutdown - Commencing graceful shutdown. Waiting for active requests to complete +2025-11-27 19:07:48.009 [netty-shutdown] INFO [] o.s.b.w.e.netty.GracefulShutdown - Graceful shutdown complete +2025-11-27 19:07:50.020 [SpringApplicationShutdownHook] INFO [] o.s.c.c.s.ConsulServiceRegistry - Deregistering service with consul: meldestelle-8080-54f57945-6e65-4dd4-bb1d-ddec4966a68a diff --git a/services/members/README-MEMBERS.md b/services/members/README-MEMBERS.md deleted file mode 100644 index 6eb58b1d..00000000 --- a/services/members/README-MEMBERS.md +++ /dev/null @@ -1,345 +0,0 @@ -# Members Module - -## Überblick - -Das Members-Modul ist eine umfassende Lösung zur Verwaltung von Mitgliedern für Pferdesportorganisationen. Es implementiert eine saubere Architektur mit Domain-Driven Design und bietet vollständige CRUD-Operationen sowie erweiterte Geschäftslogik für die Mitgliederverwaltung. - -## Funktionalität - -### Verwaltete Entität - -#### Mitglied (Member) - -- **Persönliche Informationen**: Vor- und Nachname, E-Mail, Telefon, Geburtsdatum -- **Mitgliedschaftsinformationen**: Mitgliedsnummer, Start-/Enddatum, Aktivitätsstatus -- **Zusätzliche Informationen**: Adresse, Notfallkontakt -- **Audit-Felder**: Erstellungs- und Aktualisierungszeitstempel -- **Geschäftslogik**: Validierung, Mitgliedschaftsgültigkeit, Vollständiger Name - -### Geschäftsoperationen - -Das Modul bietet 18+ spezialisierte Repository-Operationen: - -#### Basis-CRUD-Operationen - -- `findById(id)` - Mitglied nach UUID suchen -- `save(member)` - Mitglied speichern (erstellen/aktualisieren) -- `delete(id)` - Mitglied löschen - -#### Such-Operationen - -- `findByMembershipNumber(number)` - Nach Mitgliedsnummer suchen -- `findByEmail(email)` - Nach E-Mail-Adresse suchen -- `findByName(searchTerm, limit)` - Nach Namen suchen (Teilübereinstimmung) -- `findAllActive(limit, offset)` - Alle aktiven Mitglieder -- `findAll(limit, offset)` - Alle Mitglieder (aktiv und inaktiv) - -#### Datumsbasierte Abfragen - -- `findByMembershipStartDateRange(start, end)` - Mitglieder nach Startdatum-Bereich -- `findByMembershipEndDateRange(start, end)` - Mitglieder nach Enddatum-Bereich -- `findMembersWithExpiringMembership(daysAhead)` - Mitglieder mit ablaufender Mitgliedschaft - -#### Validierungs-Operationen - -- `existsByMembershipNumber(number, excludeId)` - Prüfung auf doppelte Mitgliedsnummer -- `existsByEmail(email, excludeId)` - Prüfung auf doppelte E-Mail-Adresse - -#### Zähl-Operationen - -- `countActive()` - Anzahl aktiver Mitglieder -- `countAll()` - Gesamtanzahl aller Mitglieder - -## Architektur - -Das Modul folgt der Clean Architecture mit klarer Trennung der Verantwortlichkeiten: - -``` -members/ -├── members-domain/ # Domain Layer -│ ├── model/ # Domain Models -│ │ └── Member.kt # Mitglied-Entität mit Geschäftslogik -│ ├── repository/ # Repository Interfaces -│ │ └── MemberRepository.kt # 18+ Geschäftsoperationen -│ └── events/ # Domain Events -│ └── MemberEvents.kt # Mitgliedschafts-Events -├── members-application/ # Application Layer -│ └── usecase/ # Use Cases -│ └── FindExpiringMembershipsUseCase.kt -├── members-infrastructure/ # Infrastructure Layer -│ ├── persistence/ # Database Implementation -│ │ ├── MemberRepositoryImpl.kt -│ │ └── MemberTable.kt -│ └── repository/ # Alternative Implementations -│ └── InMemoryMemberRepository.kt -├── members-api/ # API Layer -│ └── rest/ # REST Controllers -│ └── MemberController.kt -└── members-service/ # Service Layer - ├── MembersServiceApplication.kt - └── test/ # Integration Tests - └── MemberServiceIntegrationTest.kt -``` - -### Domain Layer - -- **1 Domain Model** mit reichhaltiger Geschäftslogik -- **1 Repository Interface** mit 18+ Geschäftsoperationen -- **Domain Events** für Mitgliedschaftsänderungen -- **Keine Abhängigkeiten** zu anderen Layern - -### Application Layer - -- **Use Cases** für komplexe Geschäftsoperationen -- **Orchestrierung** von Domain-Services -- **Anwendungslogik** ohne UI-Abhängigkeiten - -### Infrastructure Layer - -- **Datenbankzugriff** mit Exposed ORM -- **Repository-Implementierung** mit PostgreSQL -- **In-Memory-Repository** für Tests -- **Datenbankschema** und Migrationen - -### API Layer - -- **REST-Controller** für HTTP-Endpunkte -- **DTO-Mapping** zwischen Domain und API -- **Validierung** und Fehlerbehandlung - -### Service Layer - -- **Spring Boot Anwendung** -- **Dependency Injection** Konfiguration -- **Integrationstests** - -## Domain Model Details - -### Member-Entität - -```kotlin -data class Member( - val memberId: Uuid, - - // Persönliche Informationen - var firstName: String, - var lastName: String, - var email: String, - var phone: String? = null, - var dateOfBirth: LocalDate? = null, - - // Mitgliedschaftsinformationen - var membershipNumber: String, - var membershipStartDate: LocalDate, - var membershipEndDate: LocalDate? = null, - var isActive: Boolean = true, - - // Zusätzliche Informationen - var address: String? = null, - var emergencyContact: String? = null, - - // Audit-Felder - val createdAt: Instant, - var updatedAt: Instant -) -``` - -### Geschäftslogik-Methoden - -- `getFullName()` - Vollständiger Name des Mitglieds -- `isMembershipValid()` - Prüfung der Mitgliedschaftsgültigkeit -- `validate()` - Datenvalidierung mit Fehlerliste -- `withUpdatedTimestamp()` - Kopie mit aktualisiertem Zeitstempel - -## Repository-Operationen - -### Erweiterte Such-Features - -```kotlin -// Mitglieder mit ablaufender Mitgliedschaft finden -val expiringMembers = memberRepository.findMembersWithExpiringMembership(30) - -// Mitglieder nach Datumsbereich suchen -val newMembers = memberRepository.findByMembershipStartDateRange( - startDate = LocalDate(2024, 1, 1), - endDate = LocalDate(2024, 12, 31) -) - -// Namenssuche mit Teilübereinstimmung -val searchResults = memberRepository.findByName("Schmidt", limit = 10) -``` - -### Validierung und Duplikatsprüfung - -```kotlin -// Prüfung auf doppelte Mitgliedsnummer -val numberExists = memberRepository.existsByMembershipNumber("M2024001") - -// Prüfung auf doppelte E-Mail (mit Ausschluss für Updates) -val emailExists = memberRepository.existsByEmail( - email = "max@example.com", - excludeMemberId = existingMember.memberId -) -``` - -## Use Cases - -### FindExpiringMembershipsUseCase - -Findet Mitglieder mit ablaufenden Mitgliedschaften und kann automatische Benachrichtigungen auslösen. - -```kotlin -class FindExpiringMembershipsUseCase( - private val memberRepository: MemberRepository -) { - suspend fun execute(daysAhead: Int = 30): List { - return memberRepository.findMembersWithExpiringMembership(daysAhead) - } -} -``` - -## API-Endpunkte - -Das Members-Modul stellt REST-Endpunkte über den MemberController bereit: - -- `GET /api/members` - Alle aktiven Mitglieder abrufen -- `GET /api/members/{id}` - Mitglied nach ID abrufen -- `GET /api/members/search?name={name}` - Mitglieder nach Namen suchen -- `GET /api/members/expiring?days={days}` - Mitglieder mit ablaufender Mitgliedschaft -- `POST /api/members` - Neues Mitglied erstellen -- `PUT /api/members/{id}` - Mitglied aktualisieren -- `DELETE /api/members/{id}` - Mitglied löschen - -## Konfiguration - -### Datenbankschema - -Das Modul verwendet eine `members`-Tabelle mit folgenden Spalten: - -- `member_id` (UUID, Primary Key) -- `first_name`, `last_name`, `email` (Required) -- `phone`, `date_of_birth` (Optional) -- `membership_number` (Unique) -- `membership_start_date`, `membership_end_date` -- `is_active` (Boolean) -- `address`, `emergency_contact` (Optional) -- `created_at`, `updated_at` (Timestamps) - -### Service-Konfiguration - -```yaml -# application.yml -members: - service: - name: members-service - port: 8082 - database: - url: jdbc:postgresql://localhost:5432/meldestelle - table: members -``` - -## Tests - -### Integration Tests - -Das Modul enthält umfassende Integrationstests: - -```kotlin -@Test -fun `should find members with expiring membership`() { - // Test-Implementierung für ablaufende Mitgliedschaften -} - -@Test -fun `should validate unique membership number`() { - // Test für Eindeutigkeit der Mitgliedsnummer -} -``` - -### Test-Datenbank - -Verwendet H2 In-Memory-Datenbank für Tests mit automatischem Schema-Setup. - -## Deployment - -### Docker - -```dockerfile -FROM openjdk:21-jre-slim -COPY members-service.jar app.jar -EXPOSE 8082 -ENTRYPOINT ["java", "-jar", "/app.jar"] -``` - -### Kubernetes - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: members-service -spec: - replicas: 2 - selector: - matchLabels: - app: members-service - template: - spec: - containers: - - name: members-service - image: meldestelle/members-service:latest - ports: - - containerPort: 8082 -``` - -## Monitoring - -### Metriken - -- Anzahl aktiver Mitglieder -- Anzahl ablaufender Mitgliedschaften -- API-Response-Zeiten -- Datenbankverbindungs-Pool - -### Health Checks - -- Datenbankverbindung -- Service-Verfügbarkeit -- Speicherverbrauch - -## Entwicklung - -### Lokale Entwicklung - -```bash -# Service starten -./gradlew :members:members-service:bootRun - -# Tests ausführen -./gradlew :members:test - -# Integration Tests -./gradlew :members:members-service:test -``` - -### Code-Qualität - -- **Kotlin Coding Standards** -- **100% Test Coverage** für Domain Layer -- **Integration Tests** für alle Use Cases -- **API-Dokumentation** mit OpenAPI - -## Zukünftige Erweiterungen - -1. **Mitgliedschaftstypen** - Verschiedene Mitgliedschaftskategorien -2. **Beitragsverwaltung** - Integration mit Zahlungssystem -3. **Mitgliedschaftshistorie** - Tracking von Änderungen -4. **Bulk-Operationen** - Massenimport/-export -5. **Benachrichtigungen** - Automatische E-Mail-Benachrichtigungen -6. **Reporting** - Mitgliedschaftsstatistiken und Reports - ---- - -**Letzte Aktualisierung**: 25. Juli 2025 - -Für weitere Informationen zur Gesamtarchitektur siehe [README.md](../../README.md). diff --git a/services/members/members-api/build.gradle.kts b/services/members/members-api/build.gradle.kts deleted file mode 100644 index 3a9750d1..00000000 --- a/services/members/members-api/build.gradle.kts +++ /dev/null @@ -1,30 +0,0 @@ -plugins { -// kotlin("jvm") -// kotlin("plugin.spring") - - alias(libs.plugins.kotlinJvm) - alias(libs.plugins.kotlinSpring) - - // KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block - // und alle Spring-Boot-spezifischen Gradle-Tasks frei. - alias(libs.plugins.spring.boot) - - // Dependency Management für konsistente Spring-Versionen - alias(libs.plugins.spring.dependencyManagement) -} - -dependencies { - implementation(projects.platform.platformDependencies) - - implementation(projects.services.members.membersDomain) - implementation(projects.core.coreDomain) - implementation(projects.core.coreUtils) - - implementation("org.springframework:spring-web") - implementation("org.springdoc:springdoc-openapi-starter-common") - // Security/JWT for extracting claims from principal - implementation(libs.spring.boot.starter.security) - implementation(libs.spring.boot.starter.oauth2.resource.server) - - testImplementation(projects.platform.platformTesting) -} diff --git a/services/members/members-api/src/main/kotlin/at/mocode/members/api/rest/MemberController.kt b/services/members/members-api/src/main/kotlin/at/mocode/members/api/rest/MemberController.kt deleted file mode 100644 index ed2bceec..00000000 --- a/services/members/members-api/src/main/kotlin/at/mocode/members/api/rest/MemberController.kt +++ /dev/null @@ -1,161 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) -package at.mocode.members.api.rest - -import at.mocode.core.domain.model.ApiResponse -import at.mocode.members.domain.model.Member -import at.mocode.members.domain.repository.MemberRepository -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.tags.Tag -import kotlinx.coroutines.runBlocking -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toKotlinLocalDate -import java.time.ZoneId -import java.time.LocalDate as JLocalDate -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.security.oauth2.jwt.Jwt -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import kotlin.uuid.Uuid - -@RestController -@RequestMapping("/api/members") -@Tag(name = "Members", description = "Mitgliederverwaltungs-Operationen (minimal)") -class MemberController( - @Qualifier("memberRepositoryImpl") private val memberRepository: MemberRepository -) { - - data class SyncResponse( - val ensured: Boolean, - val created: Boolean, - val memberId: String?, - val membershipNumber: String? - ) - - data class MemberProfileDto( - val id: String? = null, - val username: String? = null, - val email: String? = null, - val firstName: String? = null, - val lastName: String? = null, - val roles: List = emptyList() - ) - - // Synchronisiert/erstellt bei Bedarf ein Member-Profil basierend auf den JWT-Claims - @Operation( - summary = "Synchronisiert das Member-Profil für den eingeloggten Benutzer", - description = "Erstellt bei Bedarf ein Mitglied basierend auf den JWT-Claims (mock OEPS fetch)" - ) - @PostMapping("/sync") - fun syncMemberProfile( - @AuthenticationPrincipal jwt: Jwt - ): ResponseEntity> { - return try { - val sub = jwt.subject - val email = jwt.getClaimAsString("email") - val username = jwt.getClaimAsString("preferred_username") - - val ensured = runBlocking { ensureMemberProfileExists(sub, email, username) } - ResponseEntity.ok(ApiResponse.success(ensured)) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("INTERNAL_ERROR", "Sync failed: ${e.message}")) - } - } - - // Liefert das Member-Profil des eingeloggten Benutzers - @Operation(summary = "Eigene Profilinformationen abrufen") - @GetMapping("/me") - fun getMyProfile( - @AuthenticationPrincipal jwt: Jwt - ): ResponseEntity> { - return try { - val sub = jwt.subject - val email = jwt.getClaimAsString("email") - val username = jwt.getClaimAsString("preferred_username") - - val member = runBlocking { - val lookupEmail = email ?: username?.let { "$it@local" } ?: "user-${sub.takeLast(8)}@local" - memberRepository.findByEmail(lookupEmail) - } - - val profile = if (member != null) { - MemberProfileDto( - id = member.memberId.toString(), - username = username, - email = member.email, - firstName = member.firstName, - lastName = member.lastName, - roles = emptyList() - ) - } else { - MemberProfileDto( - id = null, - username = username, - email = email, - firstName = username?.substringBefore('@')?.replaceFirstChar { it.titlecase() }, - lastName = null, - roles = emptyList() - ) - } - - ResponseEntity.ok(ApiResponse.success(profile)) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("INTERNAL_ERROR", "Failed to load profile: ${e.message}")) - } - } - - private suspend fun ensureMemberProfileExists(userSub: String, email: String?, username: String?): SyncResponse { - val lookupEmail = email ?: username?.let { "$it@local" } ?: "user-${userSub.takeLast(8)}@local" - val existing = memberRepository.findByEmail(lookupEmail) - if (existing != null) { - return SyncResponse( - ensured = true, - created = false, - memberId = existing.memberId.toString(), - membershipNumber = existing.membershipNumber - ) - } - - val (firstName, lastName) = mockFetchNames(userSub, email, username) - val today: LocalDate = JLocalDate.now(ZoneId.systemDefault()).toKotlinLocalDate() - val generatedMembershipNumber = "AUTO-${userSub.takeLast(8)}" - - val newMember = Member( - firstName = firstName, - lastName = lastName, - email = lookupEmail, - membershipNumber = generatedMembershipNumber, - membershipStartDate = today, - phone = null, - dateOfBirth = null, - membershipEndDate = null, - isActive = true, - address = null, - emergencyContact = null - ) - - val saved = memberRepository.save(newMember) - return SyncResponse( - ensured = true, - created = true, - memberId = saved.memberId.toString(), - membershipNumber = saved.membershipNumber - ) - } - - private fun mockFetchNames(sub: String, email: String?, username: String?): Pair { - val source = username ?: email ?: sub - val base = source.substringBefore('@').replace(".", " ").trim().ifBlank { "Reiter" } - val parts = base.split(" ") - val first = parts.firstOrNull()?.replaceFirstChar { it.titlecase() } ?: "Reiter" - val last = parts.drop(1).joinToString(" ").ifBlank { "Unbekannt" }.replaceFirstChar { it.titlecase() } - return first to last - } -} diff --git a/services/members/members-application/build.gradle.kts b/services/members/members-application/build.gradle.kts deleted file mode 100644 index 5c587869..00000000 --- a/services/members/members-application/build.gradle.kts +++ /dev/null @@ -1,11 +0,0 @@ -plugins { - kotlin("jvm") -} - -dependencies { - implementation(projects.services.members.membersDomain) - implementation(projects.core.coreDomain) - implementation(projects.core.coreUtils) - implementation(projects.infrastructure.messaging.messagingClient) - testImplementation(projects.platform.platformTesting) -} diff --git a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/CreateMemberUseCase.kt b/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/CreateMemberUseCase.kt deleted file mode 100644 index 02d58430..00000000 --- a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/CreateMemberUseCase.kt +++ /dev/null @@ -1,240 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) -package at.mocode.members.application.usecase - -import at.mocode.core.domain.model.ApiResponse -import at.mocode.core.domain.model.ErrorDto -import at.mocode.members.domain.model.Member -import at.mocode.members.domain.repository.MemberRepository -import at.mocode.members.domain.events.MemberCreatedEvent -import at.mocode.infrastructure.messaging.client.EventPublisher -import at.mocode.core.domain.model.ValidationResult -import at.mocode.core.domain.model.ValidationError -import kotlin.uuid.Uuid -import kotlinx.datetime.Clock -import kotlinx.datetime.LocalDate - -/** - * Use case for creating new members. - * - * This use case handles the business logic for creating members, - * including validation and persistence. - */ -class CreateMemberUseCase( - private val memberRepository: MemberRepository, - private val eventPublisher: EventPublisher -) { - - /** - * Request data for creating a new member. - */ - data class CreateMemberRequest( - val firstName: String, - val lastName: String, - val email: String, - val phone: String? = null, - val dateOfBirth: LocalDate? = null, - val membershipNumber: String, - val membershipStartDate: LocalDate, - val membershipEndDate: LocalDate? = null, - val isActive: Boolean = true, - val address: String? = null, - val emergencyContact: String? = null - ) - - /** - * Response data containing the created member. - */ - data class CreateMemberResponse( - val member: Member - ) - - /** - * Executes the create member use case. - * - * @param request The request containing member data - * @return ApiResponse with the created member or error information - */ - suspend fun execute(request: CreateMemberRequest): ApiResponse { - return try { - // Validate the request - val validationResult = validateRequest(request) - if (!validationResult.isValid()) { - val errors = (validationResult as ValidationResult.Invalid).errors - return ApiResponse( - success = false, - error = ErrorDto( - code = "VALIDATION_ERROR", - message = "Invalid input data", - details = errors.associate { it.field to it.message } - ) - ) - } - - // Check for duplicate membership number - if (memberRepository.existsByMembershipNumber(request.membershipNumber)) { - return ApiResponse( - success = false, - error = ErrorDto( - code = "DUPLICATE_MEMBERSHIP_NUMBER", - message = "Membership number already exists" - ) - ) - } - - // Check for duplicate email - if (memberRepository.existsByEmail(request.email)) { - return ApiResponse( - success = false, - error = ErrorDto( - code = "DUPLICATE_EMAIL", - message = "Email address already exists" - ) - ) - } - - // Create the domain object - val member = Member( - firstName = request.firstName.trim(), - lastName = request.lastName.trim(), - email = request.email.trim().lowercase(), - phone = request.phone?.trim(), - dateOfBirth = request.dateOfBirth, - membershipNumber = request.membershipNumber.trim(), - membershipStartDate = request.membershipStartDate, - membershipEndDate = request.membershipEndDate, - isActive = request.isActive, - address = request.address?.trim(), - emergencyContact = request.emergencyContact?.trim(), - createdAt = Clock.System.now(), - updatedAt = Clock.System.now() - ) - - // Validate the domain object - val domainValidationErrors = member.validate() - if (domainValidationErrors.isNotEmpty()) { - return ApiResponse( - success = false, - error = ErrorDto( - code = "DOMAIN_VALIDATION_ERROR", - message = "Domain validation failed", - details = domainValidationErrors.mapIndexed { index, error -> - "error_$index" to error - }.toMap() - ) - ) - } - - // Save the member - val savedMember = memberRepository.save(member) - - // Publish member created event - try { - val event = MemberCreatedEvent( - eventId = Uuid.random().toString(), - memberId = savedMember.memberId, - timestamp = Clock.System.now(), - firstName = savedMember.firstName, - lastName = savedMember.lastName, - email = savedMember.email, - membershipNumber = savedMember.membershipNumber, - membershipStartDate = savedMember.membershipStartDate, - isActive = savedMember.isActive - ) - eventPublisher.publishEvent("member-events", savedMember.memberId.toString(), event) - } catch (e: Exception) { - // Log the error but don't fail the operation - // In a production system, you might want to use a dead letter queue or retry mechanism - println("Failed to publish member created event: ${e.message}") - } - - ApiResponse( - success = true, - data = CreateMemberResponse(savedMember) - ) - - } catch (e: Exception) { - ApiResponse( - success = false, - error = ErrorDto( - code = "INTERNAL_ERROR", - message = "Failed to create member: ${e.message}" - ) - ) - } - } - - /** - * Validates the create member request. - */ - private fun validateRequest(request: CreateMemberRequest): ValidationResult { - val errors = mutableListOf() - - // Validate first name - if (request.firstName.isBlank()) { - errors.add(ValidationError("firstName", "First name is required")) - } else if (request.firstName.length > 100) { - errors.add(ValidationError("firstName", "First name must not exceed 100 characters")) - } - - // Validate last name - if (request.lastName.isBlank()) { - errors.add(ValidationError("lastName", "Last name is required")) - } else if (request.lastName.length > 100) { - errors.add(ValidationError("lastName", "Last name must not exceed 100 characters")) - } - - // Validate email - if (request.email.isBlank()) { - errors.add(ValidationError("email", "Email is required")) - } else if (!isValidEmail(request.email)) { - errors.add(ValidationError("email", "Email format is invalid")) - } else if (request.email.length > 255) { - errors.add(ValidationError("email", "Email must not exceed 255 characters")) - } - - // Validate membership number - if (request.membershipNumber.isBlank()) { - errors.add(ValidationError("membershipNumber", "Membership number is required")) - } else if (request.membershipNumber.length > 50) { - errors.add(ValidationError("membershipNumber", "Membership number must not exceed 50 characters")) - } - - // Validate membership dates - request.membershipEndDate?.let { endDate -> - if (endDate < request.membershipStartDate) { - errors.add(ValidationError("membershipEndDate", "Membership end date cannot be before start date")) - } - } - - // Validate phone - request.phone?.let { phone -> - if (phone.length > 50) { - errors.add(ValidationError("phone", "Phone number must not exceed 50 characters")) - } - } - - // Validate address - request.address?.let { address -> - if (address.length > 500) { - errors.add(ValidationError("address", "Address must not exceed 500 characters")) - } - } - - // Validate emergency contact - request.emergencyContact?.let { contact -> - if (contact.length > 255) { - errors.add(ValidationError("emergencyContact", "Emergency contact must not exceed 255 characters")) - } - } - - return if (errors.isEmpty()) { - ValidationResult.Valid - } else { - ValidationResult.Invalid(errors) - } - } - - private fun isValidEmail(email: String): Boolean { - return email.contains("@") && email.contains(".") && email.indexOf("@") < email.lastIndexOf(".") - } -} diff --git a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/DeleteMemberUseCase.kt b/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/DeleteMemberUseCase.kt deleted file mode 100644 index 8afe0974..00000000 --- a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/DeleteMemberUseCase.kt +++ /dev/null @@ -1,85 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) -package at.mocode.members.application.usecase - -import at.mocode.core.domain.model.ApiResponse -import at.mocode.core.domain.model.ErrorDto -import at.mocode.members.domain.repository.MemberRepository -import kotlin.uuid.Uuid - -/** - * Use case for deleting members. - * - * This use case handles the business logic for deleting members - * from the system. - */ -class DeleteMemberUseCase( - private val memberRepository: MemberRepository -) { - - /** - * Request data for deleting a member. - */ - data class DeleteMemberRequest( - val memberId: Uuid - ) - - /** - * Response data for delete operation. - */ - data class DeleteMemberResponse( - val success: Boolean, - val message: String - ) - - /** - * Executes the delete member use case. - * - * @param request The request containing member ID to delete - * @return ApiResponse with the result or error information - */ - suspend fun execute(request: DeleteMemberRequest): ApiResponse { - return try { - // Check if member exists - val existingMember = memberRepository.findById(request.memberId) - if (existingMember == null) { - return ApiResponse( - success = false, - error = ErrorDto( - code = "MEMBER_NOT_FOUND", - message = "Member not found" - ) - ) - } - - // Delete the member - val deleted = memberRepository.delete(request.memberId) - - if (deleted) { - ApiResponse( - success = true, - data = DeleteMemberResponse( - success = true, - message = "Member deleted successfully" - ) - ) - } else { - ApiResponse( - success = false, - error = ErrorDto( - code = "DELETE_FAILED", - message = "Failed to delete member" - ) - ) - } - - } catch (e: Exception) { - ApiResponse( - success = false, - error = ErrorDto( - code = "INTERNAL_ERROR", - message = "Failed to delete member: ${e.message}" - ) - ) - } - } -} diff --git a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/EnsureMemberProfileExistsUseCase.kt b/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/EnsureMemberProfileExistsUseCase.kt deleted file mode 100644 index 3be613d5..00000000 --- a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/EnsureMemberProfileExistsUseCase.kt +++ /dev/null @@ -1,107 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package at.mocode.members.application.usecase - -import at.mocode.infrastructure.messaging.client.EventPublisher -import at.mocode.members.domain.events.MemberCreatedEvent -import at.mocode.members.domain.model.Member -import at.mocode.members.domain.repository.MemberRepository -import kotlinx.datetime.Clock -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.todayIn -import kotlin.uuid.Uuid - -/** - * UseCase: Stellt sicher, dass für den authentifizierten Benutzer ein Member-Profil existiert. - * - * Ablauf: - * 1) Prüft, ob ein Member zu E-Mail existiert (Fallback: username-basiert via Dummy-E-Mail) - * 2) Wenn nicht vorhanden: erstellt minimalen Member-Datensatz (Mock für OEPS-Datenbezug) - * 3) Publiziert MemberCreatedEvent - */ -class EnsureMemberProfileExistsUseCase( - private val memberRepository: MemberRepository, - private val eventPublisher: EventPublisher -) { - - data class Request( - val userSub: String, - val email: String?, - val username: String? - ) - - data class Response( - val ensured: Boolean, // true, wenn jetzt auf jeden Fall vorhanden (neu oder bereits vorhanden) - val created: Boolean, // true, wenn neu angelegt - val memberId: Uuid?, - val membershipNumber: String? - ) - - suspend fun execute(request: Request): Response { - // 1) Versuche per E-Mail zu finden (stabilster Identifier). Fallback: generierte Pseudo-E-Mail aus username/sub. - val lookupEmail = request.email ?: request.username?.let { "$it@local" } - ?: "user-${request.userSub.takeLast(8)}@local" - - val existing = memberRepository.findByEmail(lookupEmail) - if (existing != null) { - return Response(ensured = true, created = false, memberId = existing.memberId, membershipNumber = existing.membershipNumber) - } - - // 2) Keine Daten vorhanden: OEPS-Daten mocken und Member erzeugen - val (firstName, lastName) = mockFetchOepsNames(request) - val today: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()) - val generatedMembershipNumber = "AUTO-${request.userSub.takeLast(8)}" - - val newMember = Member( - firstName = firstName, - lastName = lastName, - email = lookupEmail, - membershipNumber = generatedMembershipNumber, - membershipStartDate = today, - // optionale Felder leer lassen - phone = null, - dateOfBirth = null, - membershipEndDate = null, - isActive = true, - address = null, - emergencyContact = null - ) - - val saved = memberRepository.save(newMember) - - // 3) Domain-Event publizieren (best effort, Fehler sollen nicht verhindern) - runCatching { - val event = MemberCreatedEvent( - eventId = "evt-${saved.memberId}", - memberId = saved.memberId, - timestamp = Clock.System.now(), - firstName = saved.firstName, - lastName = saved.lastName, - email = saved.email, - membershipNumber = saved.membershipNumber, - membershipStartDate = saved.membershipStartDate, - isActive = saved.isActive - ) - eventPublisher.publishEvent( - topic = "members.events", - key = saved.memberId.toString(), - event = event - ) - } - - return Response(ensured = true, created = true, memberId = saved.memberId, membershipNumber = saved.membershipNumber) - } - - /** - * Mock für den späteren OEPS-Datenbezug. Leitet aus Username/E-Mail simple Namen ab. - */ - private fun mockFetchOepsNames(request: Request): Pair { - val source = request.username ?: request.email ?: request.userSub - val base = source.substringBefore('@').replace(".", " ").trim().ifBlank { "Reiter" } - val parts = base.split(" ") - val first = parts.firstOrNull()?.replaceFirstChar { it.titlecase() } ?: "Reiter" - val last = parts.drop(1).joinToString(" ").ifBlank { "Unbekannt" }.replaceFirstChar { it.titlecase() } - return first to last - } -} diff --git a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/FindExpiringMembershipsUseCase.kt b/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/FindExpiringMembershipsUseCase.kt deleted file mode 100644 index 15b48a3a..00000000 --- a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/FindExpiringMembershipsUseCase.kt +++ /dev/null @@ -1,71 +0,0 @@ -package at.mocode.members.application.usecase - -import at.mocode.core.domain.model.ApiResponse -import at.mocode.core.domain.model.ErrorDto -import at.mocode.members.domain.model.Member -import at.mocode.members.domain.repository.MemberRepository - -/** - * Use case for finding members with expiring memberships. - * - * This use case handles the business logic for finding members - * whose memberships are expiring within a specified number of days. - */ -class FindExpiringMembershipsUseCase( - private val memberRepository: MemberRepository -) { - - /** - * Request data for finding expiring memberships. - */ - data class FindExpiringMembershipsRequest( - val daysAhead: Int = 30 - ) - - /** - * Response data containing the list of members with expiring memberships. - */ - data class FindExpiringMembershipsResponse( - val members: List, - val count: Int - ) - - /** - * Executes the find expiring memberships use case. - * - * @param request The request containing the number of days to look ahead - * @return ApiResponse with the list of members or error information - */ - suspend fun execute(request: FindExpiringMembershipsRequest): ApiResponse { - return try { - // Validate input - if (request.daysAhead < 0) { - return ApiResponse( - success = false, - error = ErrorDto( - code = "INVALID_DAYS_AHEAD", - message = "Days ahead must be a positive number" - ) - ) - } - - val members = memberRepository.findMembersWithExpiringMembership(request.daysAhead) - - ApiResponse( - success = true, - data = FindExpiringMembershipsResponse( - members = members, - count = members.size - ) - ) - } catch (e: Exception) { - ApiResponse( - success = false, - error = ErrorDto( - code = "INTERNAL_ERROR", - message = "Failed to find expiring memberships: ${e.message}" - ) - ) - } - } -} diff --git a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/FindMembersByDateRangeUseCase.kt b/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/FindMembersByDateRangeUseCase.kt deleted file mode 100644 index 74702e92..00000000 --- a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/FindMembersByDateRangeUseCase.kt +++ /dev/null @@ -1,93 +0,0 @@ -package at.mocode.members.application.usecase - -import at.mocode.core.domain.model.ApiResponse -import at.mocode.core.domain.model.ErrorDto -import at.mocode.members.domain.model.Member -import at.mocode.members.domain.repository.MemberRepository -import kotlinx.datetime.LocalDate - -/** - * Use case for finding members by date ranges. - * - * This use case handles the business logic for finding members - * based on their membership start or end date ranges. - */ -class FindMembersByDateRangeUseCase( - private val memberRepository: MemberRepository -) { - - /** - * Request data for finding members by date range. - */ - data class FindMembersByDateRangeRequest( - val startDate: LocalDate, - val endDate: LocalDate, - val dateType: DateRangeType - ) - - /** - * Type of date range to search by. - */ - enum class DateRangeType { - MEMBERSHIP_START_DATE, - MEMBERSHIP_END_DATE - } - - /** - * Response data containing the list of members within the date range. - */ - data class FindMembersByDateRangeResponse( - val members: List, - val count: Int, - val dateType: DateRangeType, - val startDate: LocalDate, - val endDate: LocalDate - ) - - /** - * Executes the find members by date range use case. - * - * @param request The request containing the date range and type - * @return ApiResponse with the list of members or error information - */ - suspend fun execute(request: FindMembersByDateRangeRequest): ApiResponse { - return try { - // Validate input - if (request.startDate > request.endDate) { - return ApiResponse( - success = false, - error = ErrorDto( - code = "INVALID_DATE_RANGE", - message = "Start date cannot be after end date" - ) - ) - } - - val members = when (request.dateType) { - DateRangeType.MEMBERSHIP_START_DATE -> - memberRepository.findByMembershipStartDateRange(request.startDate, request.endDate) - DateRangeType.MEMBERSHIP_END_DATE -> - memberRepository.findByMembershipEndDateRange(request.startDate, request.endDate) - } - - ApiResponse( - success = true, - data = FindMembersByDateRangeResponse( - members = members, - count = members.size, - dateType = request.dateType, - startDate = request.startDate, - endDate = request.endDate - ) - ) - } catch (e: Exception) { - ApiResponse( - success = false, - error = ErrorDto( - code = "INTERNAL_ERROR", - message = "Failed to find members by date range: ${e.message}" - ) - ) - } - } -} diff --git a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/GetMemberUseCase.kt b/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/GetMemberUseCase.kt deleted file mode 100644 index 46fd3297..00000000 --- a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/GetMemberUseCase.kt +++ /dev/null @@ -1,132 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) -package at.mocode.members.application.usecase - -import at.mocode.core.domain.model.ApiResponse -import at.mocode.core.domain.model.ErrorDto -import at.mocode.members.domain.model.Member -import at.mocode.members.domain.repository.MemberRepository -import kotlin.uuid.Uuid - -/** - * Use case for retrieving members. - * - * This use case handles the business logic for retrieving members - * by various criteria. - */ -class GetMemberUseCase( - private val memberRepository: MemberRepository -) { - - /** - * Request data for getting a member by ID. - */ - data class GetMemberRequest( - val memberId: Uuid - ) - - /** - * Response data containing the retrieved member. - */ - data class GetMemberResponse( - val member: Member - ) - - /** - * Executes the get member use case. - * - * @param request The request containing member ID - * @return ApiResponse with the member or error information - */ - suspend fun execute(request: GetMemberRequest): ApiResponse { - return try { - val member = memberRepository.findById(request.memberId) - - if (member != null) { - ApiResponse( - success = true, - data = GetMemberResponse(member) - ) - } else { - ApiResponse( - success = false, - error = ErrorDto( - code = "MEMBER_NOT_FOUND", - message = "Member not found" - ) - ) - } - } catch (e: Exception) { - ApiResponse( - success = false, - error = ErrorDto( - code = "INTERNAL_ERROR", - message = "Failed to retrieve member: ${e.message}" - ) - ) - } - } - - /** - * Gets a member by membership number. - */ - suspend fun getByMembershipNumber(membershipNumber: String): ApiResponse { - return try { - val member = memberRepository.findByMembershipNumber(membershipNumber) - - if (member != null) { - ApiResponse( - success = true, - data = GetMemberResponse(member) - ) - } else { - ApiResponse( - success = false, - error = ErrorDto( - code = "MEMBER_NOT_FOUND", - message = "Member not found" - ) - ) - } - } catch (e: Exception) { - ApiResponse( - success = false, - error = ErrorDto( - code = "INTERNAL_ERROR", - message = "Failed to retrieve member: ${e.message}" - ) - ) - } - } - - /** - * Gets a member by email address. - */ - suspend fun getByEmail(email: String): ApiResponse { - return try { - val member = memberRepository.findByEmail(email) - - if (member != null) { - ApiResponse( - success = true, - data = GetMemberResponse(member) - ) - } else { - ApiResponse( - success = false, - error = ErrorDto( - code = "MEMBER_NOT_FOUND", - message = "Member not found" - ) - ) - } - } catch (e: Exception) { - ApiResponse( - success = false, - error = ErrorDto( - code = "INTERNAL_ERROR", - message = "Failed to retrieve member: ${e.message}" - ) - ) - } - } -} diff --git a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/UpdateMemberUseCase.kt b/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/UpdateMemberUseCase.kt deleted file mode 100644 index 4ea22986..00000000 --- a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/UpdateMemberUseCase.kt +++ /dev/null @@ -1,227 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) -package at.mocode.members.application.usecase - -import at.mocode.core.domain.model.ApiResponse -import at.mocode.core.domain.model.ErrorDto -import at.mocode.members.domain.model.Member -import at.mocode.members.domain.repository.MemberRepository -import at.mocode.core.domain.model.ValidationResult -import at.mocode.core.domain.model.ValidationError -import kotlin.uuid.Uuid -import kotlinx.datetime.LocalDate - -/** - * Use case for updating existing members. - * - * This use case handles the business logic for updating members, - * including validation and persistence. - */ -class UpdateMemberUseCase( - private val memberRepository: MemberRepository -) { - - /** - * Request data for updating a member. - */ - data class UpdateMemberRequest( - val memberId: Uuid, - val firstName: String, - val lastName: String, - val email: String, - val phone: String? = null, - val dateOfBirth: LocalDate? = null, - val membershipNumber: String, - val membershipStartDate: LocalDate, - val membershipEndDate: LocalDate? = null, - val isActive: Boolean = true, - val address: String? = null, - val emergencyContact: String? = null - ) - - /** - * Response data containing the updated member. - */ - data class UpdateMemberResponse( - val member: Member - ) - - /** - * Executes the update member use case. - * - * @param request The request containing updated member data - * @return ApiResponse with the updated member or error information - */ - suspend fun execute(request: UpdateMemberRequest): ApiResponse { - return try { - // Check if member exists - val existingMember = memberRepository.findById(request.memberId) - if (existingMember == null) { - return ApiResponse( - success = false, - error = ErrorDto( - code = "MEMBER_NOT_FOUND", - message = "Member not found" - ) - ) - } - - // Validate the request - val validationResult = validateRequest(request) - if (!validationResult.isValid()) { - val errors = (validationResult as ValidationResult.Invalid).errors - return ApiResponse( - success = false, - error = ErrorDto( - code = "VALIDATION_ERROR", - message = "Invalid input data", - details = errors.associate { it.field to it.message } - ) - ) - } - - // Check for duplicate membership number (excluding current member) - if (memberRepository.existsByMembershipNumber(request.membershipNumber, request.memberId)) { - return ApiResponse( - success = false, - error = ErrorDto( - code = "DUPLICATE_MEMBERSHIP_NUMBER", - message = "Membership number already exists" - ) - ) - } - - // Check for duplicate email (excluding current member) - if (memberRepository.existsByEmail(request.email, request.memberId)) { - return ApiResponse( - success = false, - error = ErrorDto( - code = "DUPLICATE_EMAIL", - message = "Email address already exists" - ) - ) - } - - // Update the member - val updatedMember = existingMember.copy( - firstName = request.firstName.trim(), - lastName = request.lastName.trim(), - email = request.email.trim().lowercase(), - phone = request.phone?.trim(), - dateOfBirth = request.dateOfBirth, - membershipNumber = request.membershipNumber.trim(), - membershipStartDate = request.membershipStartDate, - membershipEndDate = request.membershipEndDate, - isActive = request.isActive, - address = request.address?.trim(), - emergencyContact = request.emergencyContact?.trim() - ).withUpdatedTimestamp() - - // Validate the domain object - val domainValidationErrors = updatedMember.validate() - if (domainValidationErrors.isNotEmpty()) { - return ApiResponse( - success = false, - error = ErrorDto( - code = "DOMAIN_VALIDATION_ERROR", - message = "Domain validation failed", - details = domainValidationErrors.mapIndexed { index, error -> - "error_$index" to error - }.toMap() - ) - ) - } - - // Save the updated member - val savedMember = memberRepository.save(updatedMember) - - ApiResponse( - success = true, - data = UpdateMemberResponse(savedMember) - ) - - } catch (e: Exception) { - ApiResponse( - success = false, - error = ErrorDto( - code = "INTERNAL_ERROR", - message = "Failed to update member: ${e.message}" - ) - ) - } - } - - /** - * Validates the update member request. - */ - private fun validateRequest(request: UpdateMemberRequest): ValidationResult { - val errors = mutableListOf() - - // Validate first name - if (request.firstName.isBlank()) { - errors.add(ValidationError("firstName", "First name is required")) - } else if (request.firstName.length > 100) { - errors.add(ValidationError("firstName", "First name must not exceed 100 characters")) - } - - // Validate last name - if (request.lastName.isBlank()) { - errors.add(ValidationError("lastName", "Last name is required")) - } else if (request.lastName.length > 100) { - errors.add(ValidationError("lastName", "Last name must not exceed 100 characters")) - } - - // Validate email - if (request.email.isBlank()) { - errors.add(ValidationError("email", "Email is required")) - } else if (!isValidEmail(request.email)) { - errors.add(ValidationError("email", "Email format is invalid")) - } else if (request.email.length > 255) { - errors.add(ValidationError("email", "Email must not exceed 255 characters")) - } - - // Validate membership number - if (request.membershipNumber.isBlank()) { - errors.add(ValidationError("membershipNumber", "Membership number is required")) - } else if (request.membershipNumber.length > 50) { - errors.add(ValidationError("membershipNumber", "Membership number must not exceed 50 characters")) - } - - // Validate membership dates - request.membershipEndDate?.let { endDate -> - if (endDate < request.membershipStartDate) { - errors.add(ValidationError("membershipEndDate", "Membership end date cannot be before start date")) - } - } - - // Validate phone - request.phone?.let { phone -> - if (phone.length > 50) { - errors.add(ValidationError("phone", "Phone number must not exceed 50 characters")) - } - } - - // Validate address - request.address?.let { address -> - if (address.length > 500) { - errors.add(ValidationError("address", "Address must not exceed 500 characters")) - } - } - - // Validate emergency contact - request.emergencyContact?.let { contact -> - if (contact.length > 255) { - errors.add(ValidationError("emergencyContact", "Emergency contact must not exceed 255 characters")) - } - } - - return if (errors.isEmpty()) { - ValidationResult.Valid - } else { - ValidationResult.Invalid(errors) - } - } - - private fun isValidEmail(email: String): Boolean { - return email.contains("@") && email.contains(".") && email.indexOf("@") < email.lastIndexOf(".") - } -} diff --git a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/ValidateMemberDataUseCase.kt b/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/ValidateMemberDataUseCase.kt deleted file mode 100644 index cb180318..00000000 --- a/services/members/members-application/src/main/kotlin/at/mocode/members/application/usecase/ValidateMemberDataUseCase.kt +++ /dev/null @@ -1,147 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) -package at.mocode.members.application.usecase - -import at.mocode.core.domain.model.ApiResponse -import at.mocode.core.domain.model.ErrorDto -import at.mocode.members.domain.repository.MemberRepository -import kotlin.uuid.Uuid - -/** - * Use case for validating member data. - * - * This use case handles the business logic for validating - * member data such as email and membership number uniqueness. - */ -class ValidateMemberDataUseCase( - private val memberRepository: MemberRepository -) { - - /** - * Request data for validating email uniqueness. - */ - data class ValidateEmailRequest( - val email: String, - val excludeMemberId: Uuid? = null - ) - - /** - * Request data for validating membership number uniqueness. - */ - data class ValidateMembershipNumberRequest( - val membershipNumber: String, - val excludeMemberId: Uuid? = null - ) - - /** - * Response data for validation results. - */ - data class ValidationResponse( - val isValid: Boolean, - val exists: Boolean, - val message: String - ) - - /** - * Validates if an email address is unique. - * - * @param request The request containing email and optional member ID to exclude - * @return ApiResponse with validation result - */ - suspend fun validateEmail(request: ValidateEmailRequest): ApiResponse { - return try { - // Basic email format validation - if (request.email.isBlank()) { - return ApiResponse( - success = true, - data = ValidationResponse( - isValid = false, - exists = false, - message = "Email is required" - ) - ) - } - - if (!isValidEmailFormat(request.email)) { - return ApiResponse( - success = true, - data = ValidationResponse( - isValid = false, - exists = false, - message = "Email format is invalid" - ) - ) - } - - val exists = memberRepository.existsByEmail(request.email, request.excludeMemberId) - - ApiResponse( - success = true, - data = ValidationResponse( - isValid = !exists, - exists = exists, - message = if (exists) "Email already exists" else "Email is available" - ) - ) - } catch (e: Exception) { - ApiResponse( - success = false, - error = ErrorDto( - code = "INTERNAL_ERROR", - message = "Failed to validate email: ${e.message}" - ) - ) - } - } - - /** - * Validates if a membership number is unique. - * - * @param request The request containing membership number and optional member ID to exclude - * @return ApiResponse with validation result - */ - suspend fun validateMembershipNumber(request: ValidateMembershipNumberRequest): ApiResponse { - return try { - // Basic membership number validation - if (request.membershipNumber.isBlank()) { - return ApiResponse( - success = true, - data = ValidationResponse( - isValid = false, - exists = false, - message = "Membership number is required" - ) - ) - } - - val exists = memberRepository.existsByMembershipNumber(request.membershipNumber, request.excludeMemberId) - - ApiResponse( - success = true, - data = ValidationResponse( - isValid = !exists, - exists = exists, - message = if (exists) "Membership number already exists" else "Membership number is available" - ) - ) - } catch (e: Exception) { - ApiResponse( - success = false, - error = ErrorDto( - code = "INTERNAL_ERROR", - message = "Failed to validate membership number: ${e.message}" - ) - ) - } - } - - /** - * Basic email format validation. - */ - private fun isValidEmailFormat(email: String): Boolean { - return email.contains("@") && - email.contains(".") && - email.indexOf("@") > 0 && - email.lastIndexOf(".") > email.indexOf("@") && - email.length > 5 - } -} diff --git a/services/members/members-domain/build.gradle.kts b/services/members/members-domain/build.gradle.kts deleted file mode 100644 index c9be78e5..00000000 --- a/services/members/members-domain/build.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - kotlin("jvm") -} - -dependencies { - implementation(projects.core.coreDomain) - implementation(projects.core.coreUtils) - testImplementation(projects.platform.platformTesting) -} diff --git a/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/events/MemberEvents.kt b/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/events/MemberEvents.kt deleted file mode 100644 index 3105f964..00000000 --- a/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/events/MemberEvents.kt +++ /dev/null @@ -1,83 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class, kotlin.time.ExperimentalTime::class) -package at.mocode.members.domain.events - -import kotlin.uuid.Uuid -import kotlin.time.Instant -import kotlinx.datetime.LocalDate - -/** - * Base interface for all member domain events. - */ -sealed interface MemberEvent { - val eventId: String - val memberId: Uuid - val timestamp: Instant - val eventType: String -} - -/** - * Event published when a new member is created. - */ -data class MemberCreatedEvent( - override val eventId: String, - override val memberId: Uuid, - override val timestamp: Instant, - val firstName: String, - val lastName: String, - val email: String, - val membershipNumber: String, - val membershipStartDate: LocalDate, - val isActive: Boolean -) : MemberEvent { - override val eventType: String = "MemberCreated" -} - -/** - * Event published when a member is updated. - */ -data class MemberUpdatedEvent( - override val eventId: String, - override val memberId: Uuid, - override val timestamp: Instant, - val firstName: String, - val lastName: String, - val email: String, - val membershipNumber: String, - val membershipStartDate: LocalDate, - val membershipEndDate: LocalDate?, - val isActive: Boolean, - val changes: Map -) : MemberEvent { - override val eventType: String = "MemberUpdated" -} - -/** - * Event published when a member is deleted. - */ -data class MemberDeletedEvent( - override val eventId: String, - override val memberId: Uuid, - override val timestamp: Instant, - val membershipNumber: String, - val firstName: String, - val lastName: String -) : MemberEvent { - override val eventType: String = "MemberDeleted" -} - -/** - * Event published when a member's membership is about to expire. - */ -data class MembershipExpiringEvent( - override val eventId: String, - override val memberId: Uuid, - override val timestamp: Instant, - val membershipNumber: String, - val firstName: String, - val lastName: String, - val email: String, - val membershipEndDate: LocalDate, - val daysUntilExpiry: Int -) : MemberEvent { - override val eventType: String = "MembershipExpiring" -} diff --git a/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/model/Member.kt b/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/model/Member.kt deleted file mode 100644 index 528dfa4a..00000000 --- a/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/model/Member.kt +++ /dev/null @@ -1,134 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class, kotlin.time.ExperimentalTime::class) -package at.mocode.members.domain.model - -import at.mocode.core.domain.serialization.KotlinInstantSerializer -import at.mocode.core.domain.serialization.KotlinLocalDateSerializer -import at.mocode.core.domain.serialization.UuidSerializer -import kotlin.uuid.Uuid -import kotlin.time.Clock -import kotlin.time.Instant -import kotlinx.datetime.LocalDate -import kotlinx.datetime.toKotlinLocalDate -import java.time.ZoneId -import java.time.LocalDate as JLocalDate -import kotlinx.serialization.Serializable - -/** - * Domain model representing a member in the member management system. - * - * This entity represents a member of the organization with their personal - * information and membership details. - * - * @property memberId Unique internal identifier for this member (UUID). - * @property firstName First name of the member. - * @property lastName Last name of the member. - * @property email Email address of the member. - * @property phone Phone number of the member (optional). - * @property dateOfBirth Date of birth of the member (optional). - * @property membershipNumber Unique membership number. - * @property membershipStartDate Date when membership started. - * @property membershipEndDate Date when membership ends (optional). - * @property isActive Whether the membership is currently active. - * @property address Address of the member (optional). - * @property emergencyContact Emergency contact information (optional). - * @property createdAt Timestamp when this record was created. - * @property updatedAt Timestamp when this record was last updated. - */ -@Serializable -data class Member( - @Serializable(with = UuidSerializer::class) - val memberId: Uuid = Uuid.random(), - - // Personal Information - var firstName: String, - var lastName: String, - var email: String, - var phone: String? = null, - - @Serializable(with = KotlinLocalDateSerializer::class) - var dateOfBirth: LocalDate? = null, - - // Membership Information - var membershipNumber: String, - - @Serializable(with = KotlinLocalDateSerializer::class) - var membershipStartDate: LocalDate, - - @Serializable(with = KotlinLocalDateSerializer::class) - var membershipEndDate: LocalDate? = null, - - var isActive: Boolean = true, - - // Additional Information - var address: String? = null, - var emergencyContact: String? = null, - - // Audit Fields - @Serializable(with = KotlinInstantSerializer::class) - val createdAt: Instant = Clock.System.now(), - @Serializable(with = KotlinInstantSerializer::class) - var updatedAt: Instant = Clock.System.now() -) { - /** - * Returns the full name of the member. - */ - fun getFullName(): String { - return "$firstName $lastName" - } - - /** - * Checks if the membership is currently valid. - */ - fun isMembershipValid(): Boolean { - if (!isActive) return false - - val today = JLocalDate.now(ZoneId.systemDefault()).toKotlinLocalDate() - return membershipEndDate?.let { endDate -> - today <= endDate - } ?: true // If no end date, membership is valid indefinitely - } - - /** - * Validates that the member data is consistent. - */ - fun validate(): List { - val errors = mutableListOf() - - if (firstName.isBlank()) { - errors.add("First name is required") - } - - if (lastName.isBlank()) { - errors.add("Last name is required") - } - - if (email.isBlank()) { - errors.add("Email is required") - } else if (!isValidEmail(email)) { - errors.add("Email format is invalid") - } - - if (membershipNumber.isBlank()) { - errors.add("Membership number is required") - } - - membershipEndDate?.let { endDate -> - if (endDate < membershipStartDate) { - errors.add("Membership end date cannot be before start date") - } - } - - return errors - } - - /** - * Creates a copy of this member with updated timestamp. - */ - fun withUpdatedTimestamp(): Member { - return this.copy(updatedAt = Clock.System.now()) - } - - private fun isValidEmail(email: String): Boolean { - return email.contains("@") && email.contains(".") - } -} diff --git a/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/repository/MemberRepository.kt b/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/repository/MemberRepository.kt deleted file mode 100644 index a41432d0..00000000 --- a/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/repository/MemberRepository.kt +++ /dev/null @@ -1,140 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) -package at.mocode.members.domain.repository - -import at.mocode.members.domain.model.Member -import kotlin.uuid.Uuid -import kotlinx.datetime.LocalDate - -/** - * Repository interface for Member entities. - * - * This interface defines the contract for data access operations - * related to members in the member management bounded context. - */ -interface MemberRepository { - - /** - * Finds a member by their unique identifier. - * - * @param id The unique identifier of the member - * @return The member if found, null otherwise - */ - suspend fun findById(id: Uuid): Member? - - /** - * Finds a member by their membership number. - * - * @param membershipNumber The membership number to search for - * @return The member if found, null otherwise - */ - suspend fun findByMembershipNumber(membershipNumber: String): Member? - - /** - * Finds a member by their email address. - * - * @param email The email address to search for - * @return The member if found, null otherwise - */ - suspend fun findByEmail(email: String): Member? - - /** - * Finds members by name (partial match on first or last name). - * - * @param searchTerm The search term to match against member names - * @param limit Maximum number of results to return - * @return List of matching members - */ - suspend fun findByName(searchTerm: String, limit: Int = 50): List - - /** - * Finds all active members. - * - * @param limit Maximum number of results to return - * @param offset Number of results to skip - * @return List of active members - */ - suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List - - /** - * Finds all members (active and inactive). - * - * @param limit Maximum number of results to return - * @param offset Number of results to skip - * @return List of all members - */ - suspend fun findAll(limit: Int = 100, offset: Int = 0): List - - /** - * Finds members whose membership started within a date range. - * - * @param startDate The earliest membership start date to include - * @param endDate The latest membership start date to include - * @return List of members within the specified date range - */ - suspend fun findByMembershipStartDateRange(startDate: LocalDate, endDate: LocalDate): List - - /** - * Finds members whose membership expires within a date range. - * - * @param startDate The earliest membership end date to include - * @param endDate The latest membership end date to include - * @return List of members whose membership expires within the specified date range - */ - suspend fun findByMembershipEndDateRange(startDate: LocalDate, endDate: LocalDate): List - - /** - * Finds members with expiring memberships (within the next specified days). - * - * @param daysAhead Number of days to look ahead for expiring memberships - * @return List of members with expiring memberships - */ - suspend fun findMembersWithExpiringMembership(daysAhead: Int = 30): List - - /** - * Saves a member (insert or update). - * - * @param member The member to save - * @return The saved member - */ - suspend fun save(member: Member): Member - - /** - * Deletes a member by their ID. - * - * @param id The unique identifier of the member to delete - * @return True if the member was deleted, false if not found - */ - suspend fun delete(id: Uuid): Boolean - - /** - * Counts the number of active members. - * - * @return The number of active members - */ - suspend fun countActive(): Long - - /** - * Counts the total number of members. - * - * @return The total number of members - */ - suspend fun countAll(): Long - - /** - * Checks if a membership number already exists. - * - * @param membershipNumber The membership number to check - * @param excludeMemberId Optional member ID to exclude from the check (for updates) - * @return True if the membership number exists, false otherwise - */ - suspend fun existsByMembershipNumber(membershipNumber: String, excludeMemberId: Uuid? = null): Boolean - - /** - * Checks if an email address already exists. - * - * @param email The email address to check - * @param excludeMemberId Optional member ID to exclude from the check (for updates) - * @return True if the email exists, false otherwise - */ - suspend fun existsByEmail(email: String, excludeMemberId: Uuid? = null): Boolean -} diff --git a/services/members/members-infrastructure/build.gradle.kts b/services/members/members-infrastructure/build.gradle.kts deleted file mode 100644 index dba96703..00000000 --- a/services/members/members-infrastructure/build.gradle.kts +++ /dev/null @@ -1,31 +0,0 @@ -plugins { -// kotlin("jvm") -// kotlin("plugin.spring") -// kotlin("plugin.jpa") version "2.1.21" - - alias(libs.plugins.kotlinJvm) - alias(libs.plugins.kotlinSpring) - - // KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block - // und alle Spring-Boot-spezifischen Gradle-Tasks frei. - alias(libs.plugins.spring.boot) - - // Dependency Management für konsistente Spring-Versionen - alias(libs.plugins.spring.dependencyManagement) -} - -dependencies { - api(platform(projects.platform.platformBom)) - - implementation(projects.services.members.membersDomain) - implementation(projects.core.coreDomain) - implementation(projects.core.coreUtils) - implementation(projects.infrastructure.cache.cacheApi) - implementation(projects.infrastructure.eventStore.eventStoreApi) - implementation(projects.infrastructure.messaging.messagingClient) - - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - implementation("org.postgresql:postgresql") - - testImplementation(projects.platform.platformTesting) -} diff --git a/services/members/members-infrastructure/src/main/kotlin/at/mocode/members/infrastructure/persistence/MemberRepositoryImpl.kt b/services/members/members-infrastructure/src/main/kotlin/at/mocode/members/infrastructure/persistence/MemberRepositoryImpl.kt deleted file mode 100644 index 7198845a..00000000 --- a/services/members/members-infrastructure/src/main/kotlin/at/mocode/members/infrastructure/persistence/MemberRepositoryImpl.kt +++ /dev/null @@ -1,180 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) -package at.mocode.members.infrastructure.persistence - -import at.mocode.core.utils.database.DatabaseFactory -import at.mocode.members.domain.model.Member -import at.mocode.members.domain.repository.MemberRepository -import kotlin.uuid.Uuid -import kotlinx.datetime.Clock -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.springframework.stereotype.Repository - -/** - * Database implementation of MemberRepository using Exposed ORM. - */ -@Repository -class MemberRepositoryImpl : MemberRepository { - - override suspend fun findById(id: Uuid): Member? = DatabaseFactory.dbQuery { - MemberTable.selectAll().where { MemberTable.id eq id } - .map { rowToMember(it) } - .singleOrNull() - } - - override suspend fun findByMembershipNumber(membershipNumber: String): Member? = DatabaseFactory.dbQuery { - MemberTable.selectAll().where { MemberTable.membershipNumber eq membershipNumber } - .map { rowToMember(it) } - .singleOrNull() - } - - override suspend fun findByEmail(email: String): Member? = DatabaseFactory.dbQuery { - MemberTable.selectAll().where { MemberTable.email.lowerCase() eq email.lowercase() } - .map { rowToMember(it) } - .singleOrNull() - } - - override suspend fun findByName(searchTerm: String, limit: Int): List = DatabaseFactory.dbQuery { - MemberTable.selectAll().where { - (MemberTable.firstName.lowerCase() like "%${searchTerm.lowercase()}%") or - (MemberTable.lastName.lowerCase() like "%${searchTerm.lowercase()}%") - } - .limit(limit) - .map { rowToMember(it) } - } - - override suspend fun findAllActive(limit: Int, offset: Int): List = DatabaseFactory.dbQuery { - MemberTable.selectAll().where { MemberTable.isActive eq true } - .limit(limit, offset.toLong()) - .map { rowToMember(it) } - } - - override suspend fun findAll(limit: Int, offset: Int): List = DatabaseFactory.dbQuery { - MemberTable.selectAll() - .limit(limit, offset.toLong()) - .map { rowToMember(it) } - } - - override suspend fun findByMembershipStartDateRange(startDate: LocalDate, endDate: LocalDate): List = DatabaseFactory.dbQuery { - MemberTable.selectAll().where { - (MemberTable.membershipStartDate greaterEq startDate) and - (MemberTable.membershipStartDate lessEq endDate) - } - .map { rowToMember(it) } - } - - override suspend fun findByMembershipEndDateRange(startDate: LocalDate, endDate: LocalDate): List = DatabaseFactory.dbQuery { - MemberTable.selectAll().where { - (MemberTable.membershipEndDate.isNotNull()) and - (MemberTable.membershipEndDate greaterEq startDate) and - (MemberTable.membershipEndDate lessEq endDate) - } - .map { rowToMember(it) } - } - - override suspend fun findMembersWithExpiringMembership(daysAhead: Int): List = DatabaseFactory.dbQuery { - val currentDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date - val futureDate = LocalDate(currentDate.year, currentDate.month, currentDate.dayOfMonth + daysAhead) - MemberTable.selectAll().where { - (MemberTable.membershipEndDate.isNotNull()) and - (MemberTable.membershipEndDate lessEq futureDate) and - (MemberTable.isActive eq true) - } - .map { rowToMember(it) } - } - - override suspend fun save(member: Member): Member = DatabaseFactory.dbQuery { - val existingMember = MemberTable.selectAll().where { MemberTable.id eq member.memberId }.singleOrNull() - - if (existingMember != null) { - // Update existing member - MemberTable.update({ MemberTable.id eq member.memberId }) { - it[firstName] = member.firstName - it[lastName] = member.lastName - it[email] = member.email - it[phone] = member.phone - it[dateOfBirth] = member.dateOfBirth - it[membershipNumber] = member.membershipNumber - it[membershipStartDate] = member.membershipStartDate - it[membershipEndDate] = member.membershipEndDate - it[isActive] = member.isActive - it[address] = member.address - it[emergencyContact] = member.emergencyContact - it[updatedAt] = Clock.System.now() - } - } else { - // Insert new member - MemberTable.insert { - it[id] = member.memberId - it[firstName] = member.firstName - it[lastName] = member.lastName - it[email] = member.email - it[phone] = member.phone - it[dateOfBirth] = member.dateOfBirth - it[membershipNumber] = member.membershipNumber - it[membershipStartDate] = member.membershipStartDate - it[membershipEndDate] = member.membershipEndDate - it[isActive] = member.isActive - it[address] = member.address - it[emergencyContact] = member.emergencyContact - } - } - member - } - - override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery { - MemberTable.deleteWhere { MemberTable.id eq id } > 0 - } - - override suspend fun countActive(): Long = DatabaseFactory.dbQuery { - MemberTable.selectAll().where { MemberTable.isActive eq true }.count() - } - - override suspend fun countAll(): Long = DatabaseFactory.dbQuery { - MemberTable.selectAll().count() - } - - override suspend fun existsByMembershipNumber(membershipNumber: String, excludeMemberId: Uuid?): Boolean = DatabaseFactory.dbQuery { - val query = if (excludeMemberId != null) { - MemberTable.selectAll().where { - (MemberTable.membershipNumber eq membershipNumber) and - (MemberTable.id neq excludeMemberId) - } - } else { - MemberTable.selectAll().where { MemberTable.membershipNumber eq membershipNumber } - } - query.count() > 0 - } - - override suspend fun existsByEmail(email: String, excludeMemberId: Uuid?): Boolean = DatabaseFactory.dbQuery { - val query = if (excludeMemberId != null) { - MemberTable.selectAll().where { - (MemberTable.email.lowerCase() eq email.lowercase()) and - (MemberTable.id neq excludeMemberId) - } - } else { - MemberTable.selectAll().where { MemberTable.email.lowerCase() eq email.lowercase() } - } - query.count() > 0 - } - - private fun rowToMember(row: ResultRow): Member { - return Member( - memberId = row[MemberTable.id], - firstName = row[MemberTable.firstName], - lastName = row[MemberTable.lastName], - email = row[MemberTable.email], - phone = row[MemberTable.phone], - dateOfBirth = row[MemberTable.dateOfBirth], - membershipNumber = row[MemberTable.membershipNumber], - membershipStartDate = row[MemberTable.membershipStartDate], - membershipEndDate = row[MemberTable.membershipEndDate], - isActive = row[MemberTable.isActive], - address = row[MemberTable.address], - emergencyContact = row[MemberTable.emergencyContact] - ) - } -} diff --git a/services/members/members-infrastructure/src/main/kotlin/at/mocode/members/infrastructure/persistence/MemberTable.kt b/services/members/members-infrastructure/src/main/kotlin/at/mocode/members/infrastructure/persistence/MemberTable.kt deleted file mode 100644 index f2ed6400..00000000 --- a/services/members/members-infrastructure/src/main/kotlin/at/mocode/members/infrastructure/persistence/MemberTable.kt +++ /dev/null @@ -1,31 +0,0 @@ -package at.mocode.members.infrastructure.persistence - -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.kotlin.datetime.date -import org.jetbrains.exposed.sql.kotlin.datetime.timestamp -import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp - -/** - * Database table definition for members in the member management context. - * - * This table stores member information including personal details, - * membership information, and contact details. - */ -object MemberTable : Table("members") { - val id = uuid("id").autoGenerate() - val firstName = varchar("first_name", 100) - val lastName = varchar("last_name", 100) - val email = varchar("email", 255).uniqueIndex() - val phone = varchar("phone", 50).nullable() - val dateOfBirth = date("date_of_birth").nullable() - val membershipNumber = varchar("membership_number", 50).uniqueIndex() - val membershipStartDate = date("membership_start_date") - val membershipEndDate = date("membership_end_date").nullable() - val isActive = bool("is_active").default(true) - val address = varchar("address", 500).nullable() - val emergencyContact = varchar("emergency_contact", 255).nullable() - val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) - val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) - - override val primaryKey = PrimaryKey(id) -} diff --git a/services/members/members-infrastructure/src/main/kotlin/at/mocode/members/infrastructure/repository/InMemoryMemberRepository.kt b/services/members/members-infrastructure/src/main/kotlin/at/mocode/members/infrastructure/repository/InMemoryMemberRepository.kt deleted file mode 100644 index 0bfd0b42..00000000 --- a/services/members/members-infrastructure/src/main/kotlin/at/mocode/members/infrastructure/repository/InMemoryMemberRepository.kt +++ /dev/null @@ -1,104 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) -package at.mocode.members.infrastructure.repository - -import at.mocode.members.domain.model.Member -import at.mocode.members.domain.repository.MemberRepository -import kotlin.uuid.Uuid -import kotlinx.datetime.LocalDate -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import org.springframework.stereotype.Repository -import java.util.concurrent.ConcurrentHashMap - -/** - * In-memory implementation of MemberRepository for development and testing purposes. - */ -@Repository -class InMemoryMemberRepository : MemberRepository { - - private val members = ConcurrentHashMap() - - override suspend fun findById(id: Uuid): Member? { - return members[id] - } - - override suspend fun findByMembershipNumber(membershipNumber: String): Member? { - return members.values.find { it.membershipNumber == membershipNumber } - } - - override suspend fun findByEmail(email: String): Member? { - return members.values.find { it.email.equals(email, ignoreCase = true) } - } - - override suspend fun findByName(searchTerm: String, limit: Int): List { - return members.values - .filter { - it.firstName.contains(searchTerm, ignoreCase = true) || - it.lastName.contains(searchTerm, ignoreCase = true) - } - .take(limit) - } - - override suspend fun findAllActive(limit: Int, offset: Int): List { - return members.values - .filter { it.isActive } - .drop(offset) - .take(limit) - } - - override suspend fun findAll(limit: Int, offset: Int): List { - return members.values - .drop(offset) - .take(limit) - } - - override suspend fun findByMembershipStartDateRange(startDate: LocalDate, endDate: LocalDate): List { - return members.values - .filter { it.membershipStartDate >= startDate && it.membershipStartDate <= endDate } - } - - override suspend fun findByMembershipEndDateRange(startDate: LocalDate, endDate: LocalDate): List { - return members.values - .filter { member -> - member.membershipEndDate?.let { memberEndDate -> - memberEndDate >= startDate && memberEndDate <= endDate - } ?: false - } - } - - override suspend fun findMembersWithExpiringMembership(daysAhead: Int): List { - // Simplified implementation - returns members with end dates set - return members.values - .filter { it.membershipEndDate != null } - } - - override suspend fun save(member: Member): Member { - members[member.memberId] = member - return member - } - - override suspend fun delete(id: Uuid): Boolean { - return members.remove(id) != null - } - - override suspend fun countActive(): Long { - return members.values.count { it.isActive }.toLong() - } - - override suspend fun countAll(): Long { - return members.size.toLong() - } - - override suspend fun existsByMembershipNumber(membershipNumber: String, excludeMemberId: Uuid?): Boolean { - return members.values.any { - it.membershipNumber == membershipNumber && it.memberId != excludeMemberId - } - } - - override suspend fun existsByEmail(email: String, excludeMemberId: Uuid?): Boolean { - return members.values.any { - it.email.equals(email, ignoreCase = true) && it.memberId != excludeMemberId - } - } -} diff --git a/services/members/members-service/build.gradle.kts b/services/members/members-service/build.gradle.kts deleted file mode 100644 index b591ea49..00000000 --- a/services/members/members-service/build.gradle.kts +++ /dev/null @@ -1,56 +0,0 @@ -plugins { -// kotlin("jvm") -// kotlin("plugin.spring") -// id("org.springframework.boot") - - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.kotlin.spring) - - // KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block - // und alle Spring-Boot-spezifischen Gradle-Tasks frei. - alias(libs.plugins.spring.boot) - - // Dependency Management für konsistente Spring-Versionen - alias(libs.plugins.spring.dependencyManagement) -} - -springBoot { - mainClass.set("at.mocode.members.service.MembersServiceApplicationKt") -} - -dependencies { - implementation(projects.platform.platformDependencies) - - implementation(projects.core.coreDomain) - implementation(projects.core.coreUtils) - implementation(projects.members.membersDomain) - implementation(projects.members.membersApplication) - implementation(projects.members.membersInfrastructure) - implementation(projects.members.membersApi) - - implementation(projects.infrastructure.cache.redisCache) - implementation(projects.infrastructure.messaging.messagingClient) - implementation(projects.infrastructure.monitoring.monitoringClient) - - implementation("org.springframework.boot:spring-boot-starter-web") - implementation("org.springframework.boot:spring-boot-starter-validation") - implementation("org.springframework.boot:spring-boot-starter-security") - implementation("org.springframework.boot:spring-boot-starter-actuator") - implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui") - - // Database dependencies - implementation("org.jetbrains.exposed:exposed-core") - implementation("org.jetbrains.exposed:exposed-dao") - implementation("org.jetbrains.exposed:exposed-jdbc") - implementation("org.jetbrains.exposed:exposed-kotlin-datetime") - implementation("com.zaxxer:HikariCP") - runtimeOnly("org.postgresql:postgresql") - testRuntimeOnly("com.h2database:h2") - - testImplementation(projects.platform.platformTesting) - testImplementation(libs.logback.classic) // SLF4J provider for tests -} - -tasks.test { - useJUnitPlatform() -} diff --git a/services/members/members-service/src/main/kotlin/at/mocode/members/service/MembersServiceApplication.kt b/services/members/members-service/src/main/kotlin/at/mocode/members/service/MembersServiceApplication.kt deleted file mode 100644 index 3714ce4a..00000000 --- a/services/members/members-service/src/main/kotlin/at/mocode/members/service/MembersServiceApplication.kt +++ /dev/null @@ -1,21 +0,0 @@ -package at.mocode.members.service - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication -import org.springframework.context.annotation.ComponentScan - -/** - * Main application class for the Members Service. - * - * This service provides APIs for managing members and their data. - */ -@SpringBootApplication -@ComponentScan(basePackages = ["at.mocode.members"]) -class MembersServiceApplication - -/** - * Main entry point for the Members Service application. - */ -fun main(args: Array) { - runApplication(*args) -} diff --git a/services/members/members-service/src/main/kotlin/at/mocode/members/service/config/MembersDatabaseConfiguration.kt b/services/members/members-service/src/main/kotlin/at/mocode/members/service/config/MembersDatabaseConfiguration.kt deleted file mode 100644 index d0b746f9..00000000 --- a/services/members/members-service/src/main/kotlin/at/mocode/members/service/config/MembersDatabaseConfiguration.kt +++ /dev/null @@ -1,104 +0,0 @@ -package at.mocode.members.service.config - -import at.mocode.core.utils.database.DatabaseConfig -import at.mocode.core.utils.database.DatabaseFactory -import at.mocode.members.infrastructure.persistence.MemberTable -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Profile -import jakarta.annotation.PostConstruct -import jakarta.annotation.PreDestroy -import org.slf4j.LoggerFactory -import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.transactions.transaction - -/** - * Database configuration for the Members Service. - * - * This configuration ensures that Database.connect() is called properly - * before any Exposed operations are performed. - */ -@Configuration -@Profile("!test") -class MembersDatabaseConfiguration { - - private val log = LoggerFactory.getLogger(MembersDatabaseConfiguration::class.java) - - @PostConstruct - fun initializeDatabase() { - log.info("Initializing database schema for Members Service...") - - try { - // Database connection is already initialized by the gateway - // Only initialize the schema for this service - transaction { - SchemaUtils.createMissingTablesAndColumns(MemberTable) - log.info("Members database schema initialized successfully") - } - } catch (e: Exception) { - log.error("Failed to initialize database schema", e) - throw e - } - } - - @PreDestroy - fun closeDatabase() { - log.info("Closing database connection for Members Service...") - try { - DatabaseFactory.close() - log.info("Database connection closed successfully") - } catch (e: Exception) { - log.error("Error closing database connection", e) - } - } -} - -/** - * Test-specific database configuration. - */ -@Configuration -@Profile("test") -class MembersTestDatabaseConfiguration { - - private val log = LoggerFactory.getLogger(MembersTestDatabaseConfiguration::class.java) - - @PostConstruct - fun initializeTestDatabase() { - log.info("Initializing test database connection for Members Service...") - - try { - // Use H2 in-memory database for tests - val testConfig = DatabaseConfig( - jdbcUrl = "jdbc:h2:mem:members_test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", - username = "sa", - password = "", - driverClassName = "org.h2.Driver", - maxPoolSize = 5, - minPoolSize = 1, - autoMigrate = true - ) - - DatabaseFactory.init(testConfig) - log.info("Test database connection initialized successfully") - - // Initialize database schema for tests - transaction { - SchemaUtils.createMissingTablesAndColumns(MemberTable) - log.info("Test members database schema initialized successfully") - } - } catch (e: Exception) { - log.error("Failed to initialize test database connection", e) - throw e - } - } - - @PreDestroy - fun closeTestDatabase() { - log.info("Closing test database connection for Members Service...") - try { - DatabaseFactory.close() - log.info("Test database connection closed successfully") - } catch (e: Exception) { - log.error("Error closing test database connection", e) - } - } -} diff --git a/services/members/members-service/src/main/kotlin/at/mocode/members/service/config/SecurityConfiguration.kt b/services/members/members-service/src/main/kotlin/at/mocode/members/service/config/SecurityConfiguration.kt deleted file mode 100644 index 30d767a9..00000000 --- a/services/members/members-service/src/main/kotlin/at/mocode/members/service/config/SecurityConfiguration.kt +++ /dev/null @@ -1,34 +0,0 @@ -package at.mocode.members.service.config - -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity -import org.springframework.security.config.http.SessionCreationPolicy -import org.springframework.security.web.SecurityFilterChain - -/** - * Security configuration for the Members Service. - * Enables method-level security for fine-grained authorization control. - */ -@Configuration -@EnableWebSecurity -@EnableMethodSecurity(prePostEnabled = true) -class SecurityConfiguration { - - @Bean - fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - return http - .csrf { it.disable() } - .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } - .authorizeHttpRequests { auth -> - auth - // Allow health check endpoints - .requestMatchers("/actuator/**", "/health/**").permitAll() - // All other endpoints require authentication (handled by method-level security) - .anyRequest().authenticated() - } - .build() - } -} diff --git a/services/members/members-service/src/test/kotlin/at/mocode/members/service/integration/MemberServiceIntegrationTest.kt b/services/members/members-service/src/test/kotlin/at/mocode/members/service/integration/MemberServiceIntegrationTest.kt deleted file mode 100644 index cee03168..00000000 --- a/services/members/members-service/src/test/kotlin/at/mocode/members/service/integration/MemberServiceIntegrationTest.kt +++ /dev/null @@ -1,245 +0,0 @@ -package at.mocode.members.service.integration - -import at.mocode.infrastructure.messaging.client.EventPublisher -import at.mocode.members.api.rest.MemberController -import at.mocode.members.domain.model.Member -import at.mocode.members.domain.repository.MemberRepository -import io.mockk.mockk -import kotlinx.coroutines.runBlocking -import kotlinx.datetime.LocalDate -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.ContextConfiguration -import org.springframework.test.context.TestPropertySource -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -/** - * Integration tests for the Members Service. - * - * These tests verify the complete functionality including - * - REST API endpoints - * - Database operations - * - Event publishing - */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ActiveProfiles("test") -@TestPropertySource(properties = [ - "spring.datasource.url=jdbc:h2:mem:testdb", - "spring.kafka.bootstrap-servers=localhost:9092" -]) -@ContextConfiguration(classes = [MemberServiceIntegrationTest.TestConfig::class]) -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MemberServiceIntegrationTest { - - @Autowired - @Qualifier("memberRepositoryImpl") - private lateinit var memberRepository: MemberRepository - - @Configuration - class TestConfig { - @Bean - fun eventPublisher(): EventPublisher = mockk(relaxed = true) - } - - @BeforeEach - fun setUp() = runBlocking { - // Clean up database before each test - // Note: In a real implementation, you might want to use @Transactional or @DirtiesContext - println("[DEBUG_LOG] Setting up test - cleaning database") - } - - @Test - fun `should create member successfully`() = runBlocking { - println("[DEBUG_LOG] Testing member creation") - - // Given - val createRequest = MemberController.CreateMemberRequest( - firstName = "John", - lastName = "Doe", - email = "john.doe@example.com", - phone = "+43123456789", - dateOfBirth = LocalDate(1990, 1, 15), - membershipNumber = "M001", - membershipStartDate = LocalDate(2024, 1, 1), - membershipEndDate = null, - isActive = true, - address = "123 Test Street, Vienna", - emergencyContact = "Jane Doe: +43987654321" - ) - - // When - val member = Member( - firstName = createRequest.firstName, - lastName = createRequest.lastName, - email = createRequest.email, - phone = createRequest.phone, - dateOfBirth = createRequest.dateOfBirth, - membershipNumber = createRequest.membershipNumber, - membershipStartDate = createRequest.membershipStartDate, - membershipEndDate = createRequest.membershipEndDate, - isActive = createRequest.isActive, - address = createRequest.address, - emergencyContact = createRequest.emergencyContact - ) - - val savedMember = memberRepository.save(member) - - // Then - assertNotNull(savedMember) - assertEquals(createRequest.firstName, savedMember.firstName) - assertEquals(createRequest.lastName, savedMember.lastName) - assertEquals(createRequest.email, savedMember.email) - assertEquals(createRequest.membershipNumber, savedMember.membershipNumber) - assertTrue(savedMember.isActive) - - println("[DEBUG_LOG] Member created successfully with ID: ${savedMember.memberId}") - } - - @Test - fun `should find member by membership number`() = runBlocking { - println("[DEBUG_LOG] Testing find member by membership number") - - // Given - val member = Member( - firstName = "Jane", - lastName = "Smith", - email = "jane.smith@example.com", - membershipNumber = "M002", - membershipStartDate = LocalDate(2024, 1, 1), - isActive = true - ) - memberRepository.save(member) - - // When - val foundMember = memberRepository.findByMembershipNumber("M002") - - // Then - assertNotNull(foundMember) - assertEquals("Jane", foundMember.firstName) - assertEquals("Smith", foundMember.lastName) - assertEquals("M002", foundMember.membershipNumber) - - println("[DEBUG_LOG] Member found by membership number: ${foundMember.memberId}") - } - - @Test - fun `should find member by email`() = runBlocking { - println("[DEBUG_LOG] Testing find member by email") - - // Given - val member = Member( - firstName = "Bob", - lastName = "Johnson", - email = "bob.johnson@example.com", - membershipNumber = "M003", - membershipStartDate = LocalDate(2024, 1, 1), - isActive = true - ) - memberRepository.save(member) - - // When - val foundMember = memberRepository.findByEmail("bob.johnson@example.com") - - // Then - assertNotNull(foundMember) - assertEquals("Bob", foundMember.firstName) - assertEquals("Johnson", foundMember.lastName) - assertEquals("bob.johnson@example.com", foundMember.email) - - println("[DEBUG_LOG] Member found by email: ${foundMember.memberId}") - } - - @Test - fun `should count active members`() = runBlocking { - println("[DEBUG_LOG] Testing count active members") - - // Given - val activeMember = Member( - firstName = "Active", - lastName = "Member", - email = "active@example.com", - membershipNumber = "M004", - membershipStartDate = LocalDate(2024, 1, 1), - isActive = true - ) - - val inactiveMember = Member( - firstName = "Inactive", - lastName = "Member", - email = "inactive@example.com", - membershipNumber = "M005", - membershipStartDate = LocalDate(2024, 1, 1), - isActive = false - ) - - memberRepository.save(activeMember) - memberRepository.save(inactiveMember) - - // When - val activeCount = memberRepository.countActive() - val totalCount = memberRepository.countAll() - - // Then - assertTrue(activeCount >= 1, "Should have at least 1 active member") - assertTrue(totalCount >= 2, "Should have at least 2 total members") - - println("[DEBUG_LOG] Active members: $activeCount, Total members: $totalCount") - } - - @Test - fun `should validate duplicate membership number`() = runBlocking { - println("[DEBUG_LOG] Testing duplicate membership number validation") - - // Given - val member1 = Member( - firstName = "First", - lastName = "Member", - email = "first@example.com", - membershipNumber = "M006", - membershipStartDate = LocalDate(2024, 1, 1), - isActive = true - ) - memberRepository.save(member1) - - // When - val exists = memberRepository.existsByMembershipNumber("M006") - - // Then - assertTrue(exists, "Should detect existing membership number") - - println("[DEBUG_LOG] Duplicate membership number validation passed") - } - - @Test - fun `should validate duplicate email`() = runBlocking { - println("[DEBUG_LOG] Testing duplicate email validation") - - // Given - val member = Member( - firstName = "Email", - lastName = "Test", - email = "email.test@example.com", - membershipNumber = "M007", - membershipStartDate = LocalDate(2024, 1, 1), - isActive = true - ) - memberRepository.save(member) - - // When - val exists = memberRepository.existsByEmail("email.test@example.com") - - // Then - assertTrue(exists, "Should detect existing email") - - println("[DEBUG_LOG] Duplicate email validation passed") - } -} diff --git a/services/members/members-service/src/test/resources/logback-test.xml b/services/members/members-service/src/test/resources/logback-test.xml deleted file mode 100644 index 379e9ea6..00000000 --- a/services/members/members-service/src/test/resources/logback-test.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - diff --git a/settings.gradle.kts b/settings.gradle.kts index c4ce904b..2f1aaaaf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,46 +2,49 @@ rootProject.name = "Meldestelle" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { - repositories { - gradlePluginPortal() - mavenCentral() - google { - mavenContent { - includeGroupAndSubgroups("androidx") - includeGroupAndSubgroups("com.android") - includeGroupAndSubgroups("com.google") - } - } - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") - maven("https://us-central1-maven.pkg.dev/varabyte-repos/public") + repositories { + gradlePluginPortal() + mavenCentral() + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } } + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://us-central1-maven.pkg.dev/varabyte-repos/public") + } } plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } dependencyResolutionManagement { - repositories { - mavenCentral() - google() - maven { url = uri("https://jitpack.io") } - maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") } - maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } - maven { url = uri("https://us-central1-maven.pkg.dev/varabyte-repos/public") } - } + repositories { + mavenCentral() + google() + maven { url = uri("https://jitpack.io") } + maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") } + maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } + maven { url = uri("https://us-central1-maven.pkg.dev/varabyte-repos/public") } + } } -// Core modules +// ========================================================================== +// CORE & PLATFORM +// ========================================================================== include(":core:core-domain") include(":core:core-utils") -// Platform modules include(":platform:platform-bom") include(":platform:platform-dependencies") include(":platform:platform-testing") -// Infrastructure modules +// ========================================================================== +// INFRASTRUCTURE +// ========================================================================== include(":infrastructure:gateway") include(":infrastructure:messaging:messaging-client") include(":infrastructure:messaging:messaging-config") @@ -52,30 +55,56 @@ include(":infrastructure:event-store:redis-event-store") include(":infrastructure:monitoring:monitoring-client") include(":infrastructure:monitoring:monitoring-server") -// Temporary modules +// ========================================================================== +// BUSINESS DOMAINS (Clean Architecture) +// ========================================================================== + +// --- EVENTS (Competition Management) --- +//include(":domains:events:events-api") +//include(":domains:events:events-common") +//include(":domains:events:events-domain") +//include(":domains:events:events-infrastructure") +//include(":domains:events:events-service") + +// --- HORSES (Horse Management) --- +//include(":domains:horses:horses-api") +//include(":domains:horses:horses-common") +//include(":domains:horses:horses-domain") +//include(":domains:horses:horses-infrastructure") +//include(":domains:horses:horses-service") + +// --- MASTERDATA (The Rulebook) --- +//include(":domains:masterdata:masterdata-api") +//include(":domains:masterdata:masterdata-common") +//include(":domains:masterdata:masterdata-domain") +//include(":domains:masterdata:masterdata-infrastructure") +//include(":domains:masterdata:masterdata-service") + +// --- REGISTRY (Single Source of Truth) --- +// Verwaltet Personen, Pferde & Vereine (ZNS Importe). +// Ersetzt das alte 'members' und 'horses' Modul. +include(":domains:registry:oeps-importer") // NEU: Der Gatekeeper für ZNS Daten +include(":domains:registry:registry-api") +include(":domains:registry:registry-domain") +include(":domains:registry:registry-service") + +// ========================================================================== +// TECHNICAL SERVICES +// ========================================================================== include(":services:ping:ping-api") include(":services:ping:ping-service") -// Client modules -include(":clients:shared") +// ========================================================================== +// CLIENTS (Frontend) +// ========================================================================== include(":clients:app") -include(":clients:ping-feature") include(":clients:auth-feature") +include(":clients:ping-feature") +include(":clients:shared") include(":clients:shared:common-ui") include(":clients:shared:navigation") -include(":clients:members-feature") -// Documentation module +// ========================================================================== +// DOCUMENTATION +// ========================================================================== include(":docs") - -/* -// Business modules (temporarily disabled - require multiplatform configuration updates) -// Note: We enable only the Members modules needed for API contracts to support the Members client feature. -*/ -// Members modules – enabled to provide the REST API contract used by the client -include(":services:members:members-domain") -// keep application out for now (mismatch with core contracts); expose API directly via repository -// include(":services:members:members-application") -include(":services:members:members-infrastructure") -include(":services:members:members-api") -// other business modules remain disabled