diff --git a/.env.example b/.env.example index 3e4c023..8988463 100644 --- a/.env.example +++ b/.env.example @@ -8,10 +8,7 @@ CORS_ORIGINS=http://localhost:5173 GOOGLE_CLIENT_ID= JWT_SECRET=fallback_secret_please_change_in_production -# Database — SQLite (auth allowlist) -SQLITE_PATH=data/dashback.db - -# Database — MSSQL (JTL-Wawi article data) +# Database — MSSQL (auth allowlist and JTL-Wawi article data) MSSQL_SERVER= MSSQL_DATABASE= MSSQL_USER= diff --git a/README.md b/README.md index f59cd8e..6537cc7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # dashback -Backend API for Dash — Google SSO auth, SQLite allowlist, and session cookies. +Backend API for Dash — Google SSO auth, MSSQL allowlist, and session cookies. ## Setup diff --git a/package-lock.json b/package-lock.json index 77c4f3d..8a6413c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "better-sqlite3": "^12.11.1", "chalk": "^5.6.2", "cookie-parser": "^1.4.7", "cross-env": "^10.1.0", @@ -20,11 +19,24 @@ "ink": "^7.1.0", "ink-text-input": "^6.0.0", "jsonwebtoken": "^9.0.3", + "libdashstore": "file:../dashdbschema", "mssql": "^12.6.0", "react": "^19.2.7", "sharp": "^0.35.2" } }, + "../dashdbschema": { + "name": "libdashstore", + "version": "0.1.0", + "license": "ISC", + "dependencies": { + "@clack/prompts": "^1.7.0", + "@hpcc-js/wasm-graphviz": "^1.28.0", + "chalk": "^6.0.0", + "dotenv": "^17.4.2", + "mssql": "^12.7.0" + } + }, "node_modules/@alcalzone/ansi-tokenize": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", @@ -993,20 +1005,6 @@ ], "license": "MIT" }, - "node_modules/better-sqlite3": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", - "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" - } - }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -1016,26 +1014,6 @@ "node": "*" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -1073,30 +1051,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1168,12 +1122,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, "node_modules/cli-boxes": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-4.0.1.tgz", @@ -1361,30 +1309,6 @@ } } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/default-browser": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", @@ -1493,15 +1417,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -1596,15 +1511,6 @@ "node": ">=0.8.x" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -1686,12 +1592,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -1743,12 +1643,6 @@ "node": ">= 0.8" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1835,12 +1729,6 @@ "node": ">= 0.4" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/google-auth-library": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", @@ -2003,12 +1891,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, "node_modules/ink": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/ink/-/ink-7.1.0.tgz", @@ -2244,6 +2126,10 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/libdashstore": { + "resolved": "../dashdbschema", + "link": true + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2350,33 +2236,6 @@ "node": ">=6" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2402,12 +2261,6 @@ "node": ">=18.19.0" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, "node_modules/native-duplexpair": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", @@ -2423,18 +2276,6 @@ "node": ">= 0.6" } }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -2576,33 +2417,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -2625,16 +2439,6 @@ "node": ">= 0.10" } }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -2674,21 +2478,6 @@ "node": ">= 0.10" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, "node_modules/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", @@ -2713,20 +2502,6 @@ "react": "^19.2.0" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -3009,51 +2784,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/slice-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-9.0.0.tgz", @@ -3137,15 +2867,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -3158,34 +2879,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tarn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", @@ -3295,18 +2988,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/type-fest": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz", @@ -3368,12 +3049,6 @@ "node": ">= 0.8" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index de36678..e3daf3f 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,9 @@ "start": "node src/index.js", "dev": "node --watch-path=./src --watch-path=./.env src/index.js", "manage-users": "node scripts/manageUsers.js", - "search-articles": "node scripts/searchArticles.js", - "get-steuer": "node scripts/getSteuer.js", - "get-categories": "node scripts/getCategories.js", "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { - "better-sqlite3": "^12.11.1", "chalk": "^5.6.2", "cookie-parser": "^1.4.7", "cross-env": "^10.1.0", @@ -27,6 +23,7 @@ "ink": "^7.1.0", "ink-text-input": "^6.0.0", "jsonwebtoken": "^9.0.3", + "libdashstore": "file:../dashdbschema", "mssql": "^12.6.0", "react": "^19.2.7", "sharp": "^0.35.2" diff --git a/scripts/getCategories.js b/scripts/getCategories.js deleted file mode 100644 index d6dd7df..0000000 --- a/scripts/getCategories.js +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useState } from 'react'; -import { render, Box, Text, useApp, useInput } from 'ink'; -import chalk from 'chalk'; -import { getCategories } from '../src/services/misc/getCategories.js'; -import { closePool } from '../src/db/mssqlClient.js'; - -import '../src/config/env.js'; - -const e = React.createElement; - -const VISIBLE_ROWS = 20; - -function flattenTree(nodes, depth = 0, out = []) { - for (const node of nodes) { - out.push({ ...node, depth }); - flattenTree(node.children, depth + 1, out); - } - return out; -} - -function formatRow(row) { - const indent = ' '.repeat(row.depth); - const count = chalk.gray(` (${row.count})`); - return `${indent}${row.cName ?? '—'}${count} ${chalk.dim(String(row.kKategorie))}`; -} - -const CategoryBrowser = ({ rows }) => { - const { exit } = useApp(); - const [selected, setSelected] = useState(0); - const windowStart = Math.max(0, Math.min(selected - Math.floor(VISIBLE_ROWS / 2), rows.length - VISIBLE_ROWS)); - const visible = rows.slice(windowStart, windowStart + VISIBLE_ROWS); - const current = rows[selected]; - - const quit = () => { - closePool() - .catch(() => {}) - .finally(() => { - exit(); - process.exit(0); - }); - }; - - useInput((input, key) => { - if (input === 'q' || (key.ctrl && input === 'c')) { - quit(); - return; - } - if (key.upArrow) { - setSelected((s) => Math.max(0, s - 1)); - } - if (key.downArrow) { - setSelected((s) => Math.min(rows.length - 1, s + 1)); - } - }); - - return e(Box, { flexDirection: 'column' }, - e(Text, null, chalk.cyan.bold(`=== Categories (${rows.length}) ===`)), - e(Box, { marginTop: 1, flexDirection: 'column' }, - visible.map((row, index) => { - const absoluteIndex = windowStart + index; - const prefix = absoluteIndex === selected ? chalk.cyan('> ') : ' '; - const label = absoluteIndex === selected - ? chalk.cyan.bold(formatRow(row)) - : formatRow(row); - return e(Text, { key: row.kKategorie }, prefix, label); - }) - ), - rows.length > VISIBLE_ROWS - ? e(Text, { dimColor: true }, `Showing ${windowStart + 1}-${windowStart + visible.length} of ${rows.length}`) - : null, - current - ? e(Box, { marginTop: 1, flexDirection: 'column' }, - e(Text, { dimColor: true }, - `kKategorie: ${current.kKategorie} parent: ${current.kOberKategorie} sort: ${current.nSort ?? '—'} articles: ${current.count}` - ) - ) - : null, - e(Box, { marginTop: 1 }, e(Text, null, chalk.gray('↑↓ navigate, q exit'))) - ); -}; - -try { - const { tree } = await getCategories(); - const rows = flattenTree(tree); - render(e(CategoryBrowser, { rows })); -} catch (error) { - console.error(chalk.red(`Error: ${error.message}`)); - await closePool().catch(() => {}); - process.exit(1); -} diff --git a/scripts/getSteuer.js b/scripts/getSteuer.js deleted file mode 100644 index 642bc0d..0000000 --- a/scripts/getSteuer.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import { render, Box, Text, useApp, useInput } from 'ink'; -import chalk from 'chalk'; -import { getSteuer } from '../src/services/misc/getSteuer.js'; -import { closePool } from '../src/db/mssqlClient.js'; - -import '../src/config/env.js'; - -const e = React.createElement; - -function SteuerTable({ steuerklassen }) { - return e(Box, { flexDirection: 'column' }, - e(Text, null, chalk.cyan.bold('=== Steuerklassen ===')), - e(Box, { marginTop: 1, flexDirection: 'column' }, - e(Text, { dimColor: true }, ' ID Rate Name'), - ...steuerklassen.map((row) => - e(Text, { key: row.kSteuerklasse }, - ' ', - chalk.white(String(row.kSteuerklasse).padEnd(4)), - chalk.yellow(`${String(row.fSteuersatz).padStart(3)}%`.padEnd(8)), - row.cName ?? '—', - row.nStandard === 1 ? chalk.gray(' (standard)') : '' - ) - ) - ), - e(Box, { marginTop: 1 }, e(Text, null, chalk.gray('Press any key to exit'))) - ); -} - -const App = ({ steuerklassen }) => { - const { exit } = useApp(); - - useInput(() => { - closePool() - .catch(() => {}) - .finally(() => { - exit(); - process.exit(0); - }); - }); - - return e(SteuerTable, { steuerklassen }); -}; - -try { - const { steuerklassen } = await getSteuer(); - render(e(App, { steuerklassen })); -} catch (error) { - console.error(chalk.red(`Error: ${error.message}`)); - await closePool().catch(() => {}); - process.exit(1); -} diff --git a/scripts/manageUsers.js b/scripts/manageUsers.js index 98f4fcb..ab27eb4 100644 --- a/scripts/manageUsers.js +++ b/scripts/manageUsers.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { render, Box, Text, useApp, useInput } from 'ink'; import TextInput from 'ink-text-input'; import chalk from 'chalk'; @@ -48,6 +48,17 @@ const App = () => { const [email, setEmail] = useState(''); const [message, setMessage] = useState(''); const [error, setError] = useState(''); + const [emails, setEmails] = useState(null); + + useEffect(() => { + if (mode !== MODES.LIST) { + return; + } + setEmails(null); + googleAuth.listAllowedEmails() + .then(setEmails) + .catch((err) => finish(false, err.message)); + }, [mode]); const finish = (success, text) => { if (success) { @@ -72,28 +83,28 @@ const App = () => { setMode(action); }; - const handleAdd = () => { + const handleAdd = async () => { try { const trimmed = email.trim(); if (!trimmed || !trimmed.includes('@')) { finish(false, 'Invalid email address'); return; } - googleAuth.addAllowedEmail(trimmed); + await googleAuth.addAllowedEmail(trimmed); finish(true, `Added ${trimmed.toLowerCase()} to google_auth`); } catch (err) { finish(false, err.message); } }; - const handleRemove = () => { + const handleRemove = async () => { try { const trimmed = email.trim(); if (!trimmed) { finish(false, 'Email is required'); return; } - const removed = googleAuth.removeAllowedEmail(trimmed); + const removed = await googleAuth.removeAllowedEmail(trimmed); finish(true, removed ? `Removed ${trimmed.toLowerCase()}` : `Email not found: ${trimmed}`); } catch (err) { finish(false, err.message); @@ -107,7 +118,9 @@ const App = () => { return e(Text, null, chalk.red(`Error: ${error}`)); } if (mode === MODES.LIST) { - const emails = googleAuth.listAllowedEmails(); + if (!emails) { + return e(Text, null, chalk.gray('Loading...')); + } if (emails.length === 0) { finish(true, 'No allowed emails in google_auth'); return e(Text, null, chalk.yellow('No allowed emails...')); diff --git a/scripts/searchArticles.js b/scripts/searchArticles.js deleted file mode 100644 index 81bec90..0000000 --- a/scripts/searchArticles.js +++ /dev/null @@ -1,401 +0,0 @@ -import React, { useState } from 'react'; -import { render, Box, Text, useApp, useInput } from 'ink'; -import TextInput from 'ink-text-input'; -import chalk from 'chalk'; -import { getArticle } from '../src/services/article/getArticle.js'; -import { searchArticles } from '../src/services/article/searchArticles.js'; -import { closePool } from '../src/db/mssqlClient.js'; -import { bruttoFromNetto, loadSteuerklassen } from '../src/services/misc/getSteuer.js'; - -import '../src/config/env.js'; - -const e = React.createElement; - -const MODES = { - SEARCH: 'search', - LOADING: 'loading', - LIST: 'list', - RESULT: 'result', - ERROR: 'error', -}; - -const VISIBLE_ROWS = 15; - -const eur = (value) => { - if (value == null || Number.isNaN(Number(value))) { - return '—'; - } - return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value); -}; - -function formatPrice(row, steuerklassen) { - if (row.fVKNEtto == null) { - return '—'; - } - const brutto = bruttoFromNetto(row.fVKNEtto, row.kSteuerklasse, steuerklassen); - return brutto == null ? '—' : eur(brutto); -} - -function hasValue(value) { - if (value == null || value === '' || value === 0) { - return false; - } - if (Array.isArray(value)) { - return value.length > 0; - } - return true; -} - -function DetailRow({ label, value, alwaysShow = false }) { - if (!alwaysShow && !hasValue(value) && value !== 0 && value !== '0') { - return null; - } - const text = value == null || value === '' ? '—' : String(value); - return e(Box, null, - e(Text, { dimColor: true }, `${label.padEnd(14)} `), - e(Text, null, text) - ); -} - -function Section({ title, children }) { - const items = React.Children.toArray(children).filter(Boolean); - if (!items.length) { - return null; - } - return e(Box, { flexDirection: 'column', marginTop: 1 }, - e(Text, null, chalk.cyan.bold(title)), - ...items - ); -} - -function ArticleDetail({ article, steuerklassen }) { - const brutto = bruttoFromNetto(article.fVKNEtto, article.kSteuerklasse, steuerklassen); - const aktiv = article.cAktiv === 'Y' - ? chalk.green('active') - : article.cAktiv === 'N' - ? chalk.red('inactive') - : article.cAktiv; - - return e(Box, { flexDirection: 'column' }, - e(Text, null, chalk.white.bold(article.cName ?? '—')), - e(Text, { dimColor: true }, article.cArtNr ?? '—'), - - e(Section, { title: 'Overview' }, - e(DetailRow, { label: 'Artikel-ID', value: article.kArtikel }), - e(DetailRow, { label: 'Status', value: aktiv }), - e(DetailRow, { label: 'Hersteller', value: article.cHersteller }), - e(DetailRow, { label: 'Barcode', value: article.cBarcode }), - article.EANs?.length - ? e(DetailRow, { label: 'EANs', value: article.EANs.map((row) => row.cEAN).join(', ') }) - : null - ), - - e(Section, { title: 'Pricing' }, - e(DetailRow, { label: 'VK brutto', value: brutto != null ? eur(brutto) : null }), - e(DetailRow, { label: 'VK netto', value: article.fVKNEtto != null ? eur(article.fVKNEtto) : null }), - e(DetailRow, { label: 'Steuerklasse', value: article.kSteuerklasse }), - hasValue(article.fUVP) ? e(DetailRow, { label: 'UVP', value: eur(article.fUVP) }) : null, - article.staffel?.length - ? e(Box, { flexDirection: 'column', marginTop: 0 }, - e(Text, { dimColor: true }, 'Staffel'), - ...article.staffel.map((row, index) => - e(Text, { key: index, dimColor: true }, ` ab ${row.menge} ${row.fVKBrutto} €`) - ) - ) - : null - ), - - e(Section, { title: 'Stock' }, - e(DetailRow, { label: 'Lager 1', value: article.fBestand, alwaysShow: true }), - hasValue(article.garage) ? e(DetailRow, { label: 'Garage', value: article.garage }) : null, - hasValue(article.keller) ? e(DetailRow, { label: 'Keller', value: article.keller }) : null, - article.fAndereLager?.length - ? e(DetailRow, { - label: 'Andere Lager', - value: article.fAndereLager.map((row) => row.fSumme).join(', '), - }) - : null, - article.incoming?.length - ? e(Text, { dimColor: true }, ` ${article.incoming.length} open supplier order(s)`) - : null - ), - - e(Section, { title: 'Product' }, - e(DetailRow, { label: 'Versandklasse', value: article.tVersandklassecName ?? article.kVersandklasse }), - hasValue(article.fGewicht) ? e(DetailRow, { label: 'Gewicht', value: `${article.fGewicht} kg` }) : null, - hasValue(article.shops) ? e(DetailRow, { label: 'Shops', value: article.shops }) : null, - article.KATs?.length - ? e(DetailRow, { label: 'Kategorien', value: article.KATs.map((row) => row.kKategorie).join(', ') }) - : null, - article.merkmale?.length - ? e(Box, { flexDirection: 'column' }, - e(Text, { dimColor: true }, 'Merkmale'), - ...article.merkmale.map((row, index) => - e(Text, { key: index, dimColor: true }, ` ${row.cName}: ${row.cWert}`) - ) - ) - : null - ), - - article.lief?.length - ? e(Section, { title: `Suppliers (${article.lief.length})` }, - ...article.lief.slice(0, 8).map((row, index) => - e(Text, { key: index, dimColor: true }, - ` ${row.nStandard ? chalk.yellow('* ') : ' '}${row.cFirma ?? '—'} ${row.cLiefArtNr ?? '—'} EK ${eur(row.fEKNetto)}` - ) - ), - article.lief.length > 8 - ? e(Text, { dimColor: true }, ` … +${article.lief.length - 8} more`) - : null - ) - : null, - - article.children?.length - ? e(Section, { title: `Variants (${article.children.length})` }, - ...article.children.slice(0, 6).map((row, index) => - e(Text, { key: index, dimColor: true }, - ` ${row.cArtNr} ${row.cName ?? '—'}` - ) - ) - ) - : null, - - article.liste?.length - ? e(Section, { title: `Stückliste (${article.liste.length})` }, - ...article.liste.map((row, index) => - e(Text, { key: index, dimColor: true }, - ` ${row.cArtNr} × ${row.fAnzahl} ${row.cName ?? '—'}` - ) - ) - ) - : null, - - article.orders?.length - ? e(Section, { title: 'Open orders' }, - ...article.orders.map((row, index) => - e(Text, { key: index, dimColor: true }, ` ${row.cType} ${row.cNote ?? ''}`) - ) - ) - : null, - - hasValue(article.cBeschreibung) - ? e(Section, { title: 'Description' }, - e(Text, null, truncateForDisplay(article.cBeschreibung, 240)) - ) - : null - ); -} - -function truncateForDisplay(value, max = 80) { - if (typeof value !== 'string' || value.length <= max) { - return value; - } - return `${value.slice(0, max)}… (${value.length} chars)`; -} - -function formatListRow(row) { - const stock = row.stock ?? '—'; - const price = row.price == null ? '—' : eur(row.price); - const name = row.name ?? '—'; - return `${row.sku ?? '—'} ${stock} ${price} ${name}`; -} - -const ResultList = ({ rows, onSelect, onBack }) => { - const [selected, setSelected] = useState(0); - const windowStart = Math.max(0, Math.min(selected - Math.floor(VISIBLE_ROWS / 2), rows.length - VISIBLE_ROWS)); - const visible = rows.slice(windowStart, windowStart + VISIBLE_ROWS); - - useInput((_input, key) => { - if (key.escape) { - onBack(); - return; - } - if (key.upArrow) { - setSelected((s) => Math.max(0, s - 1)); - } - if (key.downArrow) { - setSelected((s) => Math.min(rows.length - 1, s + 1)); - } - if (key.return && rows[selected]) { - onSelect(rows[selected]); - } - }); - - return e(Box, { flexDirection: 'column' }, - e(Text, null, chalk.cyan.bold(`${rows.length} result(s) — ↑↓ select, Enter open, Esc back`)), - visible.map((row, index) => { - const absoluteIndex = windowStart + index; - const prefix = absoluteIndex === selected ? chalk.cyan('> ') : ' '; - const label = absoluteIndex === selected - ? chalk.cyan.bold(formatListRow(row)) - : formatListRow(row); - return e(Text, { key: row.id ?? absoluteIndex }, prefix, label); - }), - rows.length > VISIBLE_ROWS - ? e(Text, null, chalk.gray(`Showing ${windowStart + 1}-${windowStart + visible.length} of ${rows.length}`)) - : null - ); -}; - -const App = ({ steuerklassen }) => { - const { exit } = useApp(); - const [mode, setMode] = useState(MODES.SEARCH); - const [target, setTarget] = useState(''); - const [rows, setRows] = useState([]); - const [article, setArticle] = useState(null); - const [error, setError] = useState(''); - const [loadingLabel, setLoadingLabel] = useState('Searching…'); - - const quit = async (code = 0) => { - try { - await closePool(); - } catch { - // ignore cleanup errors - } - exit(); - process.exit(code); - }; - - const loadArticle = async (cArtNr) => { - setLoadingLabel(`Loading ${cArtNr}…`); - setMode(MODES.LOADING); - setError(''); - - try { - const result = await getArticle(cArtNr); - if (!result) { - setError(`Article not found: ${cArtNr}`); - setMode(MODES.ERROR); - return; - } - setArticle(result); - setMode(MODES.RESULT); - } catch (err) { - setError(err.message); - setMode(MODES.ERROR); - } - }; - - const handleSearch = async () => { - const trimmed = target.trim(); - if (!trimmed) { - setError('Search terms are required'); - setMode(MODES.ERROR); - return; - } - - setLoadingLabel('Searching…'); - setMode(MODES.LOADING); - setError(''); - - try { - const { recordset } = await searchArticles(trimmed); - if (!recordset.length) { - setError(`No articles found for: ${trimmed}`); - setMode(MODES.ERROR); - return; - } - if (recordset.length === 1) { - await loadArticle(recordset[0].sku); - return; - } - setRows(recordset); - setMode(MODES.LIST); - } catch (err) { - setError(err.message); - setMode(MODES.ERROR); - } - }; - - const backToSearch = () => { - setRows([]); - setArticle(null); - setError(''); - setMode(MODES.SEARCH); - }; - - const resetSearch = () => { - setTarget(''); - backToSearch(); - }; - - if (mode === MODES.LOADING) { - return e(Text, null, chalk.yellow(loadingLabel)); - } - - if (mode === MODES.ERROR) { - return e(Box, { flexDirection: 'column' }, - e(Text, null, chalk.red(`Error: ${error}`)), - e(Box, { marginTop: 1 }, e(Text, null, chalk.gray('Press any key to try again'))), - e(ExitOnKey, { onExit: backToSearch }) - ); - } - - if (mode === MODES.LIST) { - return e(ResultList, { - rows, - onSelect: (row) => loadArticle(row.sku), - onBack: backToSearch, - }); - } - - const backToList = () => { - setArticle(null); - setError(''); - setMode(rows.length ? MODES.LIST : MODES.SEARCH); - }; - - if (mode === MODES.RESULT) { - const footer = rows.length - ? 'Esc = back to results, Enter = new search, q = quit' - : 'Enter = new search, q = quit'; - - return e(Box, { flexDirection: 'column' }, - e(ArticleDetail, { article, steuerklassen }), - e(Box, { marginTop: 1 }, e(Text, null, chalk.gray(footer))), - e(ResultInput, { - onAgain: resetSearch, - onBack: backToList, - onQuit: () => quit(0), - canGoBack: rows.length > 0, - }) - ); - } - - return e(Box, { flexDirection: 'column' }, - e(Box, { marginBottom: 1 }, - e(Text, null, chalk.cyan.bold('=== Article Search ===')) - ), - e(Text, null, chalk.gray('Partial match on name, number, EAN, barcode, supplier number')), - e(Box, { marginTop: 1 }, - e(Text, null, 'Target: '), - e(TextInput, { value: target, onChange: setTarget, onSubmit: handleSearch }) - ), - e(Box, { marginTop: 1 }, e(Text, null, chalk.gray('Enter to search, Ctrl+C to quit'))) - ); -}; - -const ResultInput = ({ onAgain, onBack, onQuit, canGoBack }) => { - useInput((_input, key) => { - if (key.escape && canGoBack) { - onBack(); - return; - } - if (_input === 'q' || (key.ctrl && _input === 'c')) { - onQuit(); - return; - } - if (key.return) { - onAgain(); - } - }); - return null; -}; - -const ExitOnKey = ({ onExit }) => { - useInput(onExit); - return null; -}; - -const steuerklassen = await loadSteuerklassen(); -render(e(App, { steuerklassen })); diff --git a/src/api/index.js b/src/api/index.js index 32be1a3..f3967fd 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -7,7 +7,7 @@ import cookieParser from 'cookie-parser'; import { loggerMiddleware } from './middleware/logger.js'; import env from '../config/env.js'; import { authMiddleware } from './middleware/auth.js'; -import { ensureConnection } from '../db/client.js'; +import { ensureConnection } from '../db/store.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/src/api/middleware/auth.js b/src/api/middleware/auth.js index 51e7914..3e819e4 100644 --- a/src/api/middleware/auth.js +++ b/src/api/middleware/auth.js @@ -6,17 +6,17 @@ import { meJson } from '../auth/meJson.js'; const userCache = new Map(); const USER_CACHE_TTL = 10 * 60 * 1000; -export function getCachedUser(id) { - const cached = userCache.get(id); +export function getCachedUser(email) { + const cached = userCache.get(email); if (cached && cached.expiresAt > Date.now()) { return cached.user; } - userCache.delete(id); + userCache.delete(email); return null; } -export function setCachedUser(id, user) { - userCache.set(id, { user, expiresAt: Date.now() + USER_CACHE_TTL }); +export function setCachedUser(email, user) { + userCache.set(email, { user, expiresAt: Date.now() + USER_CACHE_TTL }); if (userCache.size > 1000) { const now = Date.now(); for (const [k, v] of userCache) { @@ -25,8 +25,8 @@ export function setCachedUser(id, user) { } } -export function clearCachedUser(id) { - userCache.delete(id); +export function clearCachedUser(email) { + userCache.delete(email); } function authMeJson(res, req, status, body) { @@ -51,10 +51,10 @@ export const authMiddleware = async (req, res, next) => { try { const decoded = jwt.verify(token, env.jwtSecret); - let user = getCachedUser(decoded.id); + let user = getCachedUser(decoded.email); if (!user) { - user = db.getAllowedEmailById(decoded.id); - if (user) setCachedUser(decoded.id, user); + user = await db.getAllowedEmail(decoded.email); + if (user) setCachedUser(decoded.email, user); } if (!user) { return authMeJson(res, req, 403, { error: 'User is no longer authorized' }); @@ -63,6 +63,7 @@ export const authMiddleware = async (req, res, next) => { req.user = { id: user.id, email: user.email, + selectedCompanyId: user.selectedCompanyId, ...(decoded.picture && { picture: decoded.picture }), }; diff --git a/src/api/routes/article/get.js b/src/api/routes/article/get.js deleted file mode 100644 index 6f6c7cb..0000000 --- a/src/api/routes/article/get.js +++ /dev/null @@ -1,34 +0,0 @@ -import { getArticle } from '../../../services/article/getArticle.js'; - -/** - * Registers the full article lookup route backed by MSSQL. - * @param {import('express').Application} app - Express app - * @param {string} [basePath=''] - Optional URL prefix for all routes in this module - */ -export function register(app, basePath = '') { - /** - * @route GET /article/get - * @description Look up a single article by exact article number, EAN, or barcode. - * @query {string} target - Article number, packaging EAN, or barcode - * @returns {Object} 200 - `{ article }` with enriched article data - * @returns {Object} 400 - `{ error }` when target is missing - * @returns {Object} 404 - `{ error }` when no article matches - */ - app.get(`${basePath}/article/get`, async (req, res) => { - const target = req.query.target; - - if (!target || !String(target).trim()) { - return res.status(400).json({ error: 'Query parameter "target" is required' }); - } - - try { - const article = await getArticle(String(target)); - if (!article) { - return res.status(404).json({ error: 'Article not found' }); - } - return res.status(200).json({ article }); - } catch (error) { - return res.status(500).json({ error: error.message }); - } - }); -} diff --git a/src/api/routes/article/search.js b/src/api/routes/article/search.js deleted file mode 100644 index 659af98..0000000 --- a/src/api/routes/article/search.js +++ /dev/null @@ -1,36 +0,0 @@ -import { parseFilter, searchArticles } from '../../../services/article/searchArticles.js'; - -/** - * Registers the article search route backed by MSSQL. - * @param {import('express').Application} app - Express app - * @param {string} [basePath=''] - Optional URL prefix for all routes in this module - */ -export function register(app, basePath = '') { - /** - * @route GET /article/search - * @description Search articles by partial match on name, number, EAN, barcode, or supplier number. - * @query {string} target - Space-separated search terms - * @query {string} [filter] - Optional comma-separated category filter IDs (empty = all) - * @query {number} [offset=0] - Number of results to skip - * @query {number} [maxCount=10] - Maximum number of results to return - * @returns {Object} 200 - `{ categories, length, recordset }` - * @returns {Object} 400 - `{ error }` when target is missing - */ - app.get(`${basePath}/article/search`, async (req, res) => { - const target = req.query.target; - - if (!target || !String(target).trim()) { - return res.status(400).json({ error: 'Query parameter "target" is required' }); - } - - try { - const filter = parseFilter(req.query.filter); - const offset = req.query.offset !== undefined ? Number(req.query.offset) : 0; - const maxCount = req.query.maxCount !== undefined ? Number(req.query.maxCount) : 10; - const result = await searchArticles(String(target), filter, offset, maxCount); - return res.status(200).json(result); - } catch (error) { - return res.status(500).json({ error: error.message }); - } - }); -} diff --git a/src/api/routes/auth/login.js b/src/api/routes/auth/login.js index 3f7709b..51d0bd0 100644 --- a/src/api/routes/auth/login.js +++ b/src/api/routes/auth/login.js @@ -15,7 +15,7 @@ export function register(app, basePath = '') { * @route POST /auth/login * @description Verifies a Google ID token, checks allowlist, and sets an HTTP-only `token` cookie. * @body {string} idToken - Google ID token from Google Identity Services - * @returns {Object} 200 - `{ message, user }` with `{ id, email, picture? }` in `user` + * @returns {Object} 200 - `{ message, user }` with `{ id, email, selectedCompanyId, picture? }` in `user` * @returns {Object} 400 - Missing token or Google client not configured * @returns {Object} 401 - Invalid Google token * @returns {Object} 403 - Email not on allowlist @@ -43,7 +43,7 @@ export function register(app, basePath = '') { return res.status(401).json({ error: 'Invalid Google account' }); } - const allowed = db.getAllowedEmail(email); + const allowed = await db.getAllowedEmail(email); if (!allowed) { console.warn(`Login rejected: email not authorized (${email})`); return res.status(403).json({ error: 'Email is not authorized' }); @@ -52,6 +52,7 @@ export function register(app, basePath = '') { const user = { id: allowed.id, email: allowed.email, + selectedCompanyId: allowed.selectedCompanyId, ...(payload.picture && { picture: payload.picture }), }; const token = jwt.sign(user, env.jwtSecret, { expiresIn: '1d' }); diff --git a/src/api/routes/auth/logout.js b/src/api/routes/auth/logout.js index 79545ca..5538d7f 100644 --- a/src/api/routes/auth/logout.js +++ b/src/api/routes/auth/logout.js @@ -18,8 +18,8 @@ export function register(app, basePath = '') { if (token) { try { const decoded = jwt.verify(token, env.jwtSecret, { ignoreExpiration: true }); - if (decoded?.id) { - clearCachedUser(decoded.id); + if (decoded?.email) { + clearCachedUser(decoded.email); } } catch { // Ignore errors during cache clearing on logout diff --git a/src/api/routes/auth/me.js b/src/api/routes/auth/me.js index bbc121e..62f94a1 100644 --- a/src/api/routes/auth/me.js +++ b/src/api/routes/auth/me.js @@ -9,11 +9,11 @@ export function register(app, basePath = '') { /** * @route GET /auth/me * @description Returns the authenticated user from `req.user` and the Google client ID. - * @returns {Object} 200 - `{ user: { id, email, picture? }, googleClientId }` + * @returns {Object} 200 - `{ user: { id, email, selectedCompanyId, picture? }, googleClientId }` */ app.get(`${basePath}/auth/me`, (req, res) => { - const { id, email, picture } = req.user; - const user = { id, email, ...(picture && { picture }) }; + const { id, email, selectedCompanyId, picture } = req.user; + const user = { id, email, selectedCompanyId, ...(picture && { picture }) }; meJson(res, 200, { user }); }); } diff --git a/src/api/routes/health.js b/src/api/routes/health.js index 435c237..7bbdfeb 100644 --- a/src/api/routes/health.js +++ b/src/api/routes/health.js @@ -1,4 +1,4 @@ -import { ensureConnection } from '../../db/client.js'; +import { ensureConnection } from '../../db/store.js'; /** * Registers a database connectivity health check route. diff --git a/src/api/routes/misc/getCategories.js b/src/api/routes/misc/getCategories.js deleted file mode 100644 index caeddd9..0000000 --- a/src/api/routes/misc/getCategories.js +++ /dev/null @@ -1,20 +0,0 @@ -import { getCategories } from '../../../services/misc/getCategories.js'; - -/** - * @param {import('express').Application} app - * @param {string} [basePath=''] - */ -export function register(app, basePath = '') { - /** - * @route GET /misc/getCategories - * @returns {Object} 200 - `{ categories, tree }` - */ - app.get(`${basePath}/misc/getCategories`, async (_req, res) => { - try { - const result = await getCategories(); - res.status(200).json(result); - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); -} diff --git a/src/api/routes/misc/getSteuer.js b/src/api/routes/misc/getSteuer.js deleted file mode 100644 index a37d90d..0000000 --- a/src/api/routes/misc/getSteuer.js +++ /dev/null @@ -1,19 +0,0 @@ -import { getSteuer } from '../../../services/misc/getSteuer.js'; - -/** - * @param {import('express').Application} app - * @param {string} [basePath=''] - */ -export function register(app, basePath = '') { - /** - * @route GET /misc/getSteuer - * @returns {Object} 200 - `{ steuerklassen }` - */ - app.get(`${basePath}/misc/getSteuer`, async (_req, res) => { - try { - res.status(200).json(await getSteuer()); - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); -} diff --git a/src/config/env.js b/src/config/env.js index eba94cf..60646db 100644 --- a/src/config/env.js +++ b/src/config/env.js @@ -5,13 +5,6 @@ import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); dotenv.config({ path: resolve(__dirname, '../../.env'), quiet: true }); -function parseIntList(value) { - return (value || '') - .split(',') - .map((s) => Number(s.trim())) - .filter((n) => Number.isFinite(n)); -} - export default { port: Number(process.env.PORT) || 4208, corsOrigins: (process.env.CORS_ORIGINS || 'http://localhost:5173') @@ -20,7 +13,6 @@ export default { .filter(Boolean), googleClientId: process.env.GOOGLE_CLIENT_ID, jwtSecret: process.env.JWT_SECRET || 'fallback_secret_please_change_in_production', - sqlitePath: process.env.SQLITE_PATH, mssql: { server: process.env.MSSQL_SERVER, database: process.env.MSSQL_DATABASE, @@ -30,9 +22,4 @@ export default { encrypt: process.env.MSSQL_ENCRYPT === 'true', trustServerCertificate: process.env.MSSQL_TRUST_SERVER_CERTIFICATE === 'true', }, - article: { - katSkip: parseIntList(process.env.KAT_SKIP), - katLists: Number(process.env.KAT_LISTS) || 0, - }, - steuerZone: process.env.STEUER_ZONE || 'Zone-EU', }; diff --git a/src/db/client.js b/src/db/client.js deleted file mode 100644 index dce9ee0..0000000 --- a/src/db/client.js +++ /dev/null @@ -1,46 +0,0 @@ -import Database from 'better-sqlite3'; -import { mkdirSync } from 'fs'; -import { dirname, resolve } from 'path'; -import { fileURLToPath } from 'url'; -import env from '../config/env.js'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const defaultPath = resolve(__dirname, '../../data/dashback.db'); - -let db; - -function getDbPath() { - return env.sqlitePath || defaultPath; -} - -function initSchema(database) { - database.exec(` - CREATE TABLE IF NOT EXISTS google_auth ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - email TEXT NOT NULL UNIQUE COLLATE NOCASE, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ) - `); -} - -export function getDb() { - if (!db) { - const dbPath = getDbPath(); - mkdirSync(dirname(dbPath), { recursive: true }); - db = new Database(dbPath); - db.pragma('journal_mode = WAL'); - initSchema(db); - } - return db; -} - -export function ensureConnection() { - getDb().prepare('SELECT 1').get(); -} - -export function closeDb() { - if (db) { - db.close(); - db = undefined; - } -} diff --git a/src/db/googleAuth.js b/src/db/googleAuth.js index 18c321f..0137f01 100644 --- a/src/db/googleAuth.js +++ b/src/db/googleAuth.js @@ -1,34 +1,27 @@ -import { getDb } from './client.js'; +import { getStore } from './store.js'; -export function listAllowedEmails() { - return getDb() - .prepare('SELECT id, email, created_at FROM google_auth ORDER BY email COLLATE NOCASE') - .all(); +export async function listAllowedEmails() { + const store = await getStore(); + return store.google_auth.list(); } -export function getAllowedEmail(email) { - return getDb() - .prepare('SELECT id, email, created_at FROM google_auth WHERE email = ? COLLATE NOCASE') - .get(email); -} - -export function getAllowedEmailById(id) { - return getDb() - .prepare('SELECT id, email, created_at FROM google_auth WHERE id = ?') - .get(id); -} - -export function addAllowedEmail(email) { +export async function getAllowedEmail(email) { const normalized = email.trim().toLowerCase(); - const result = getDb() - .prepare('INSERT INTO google_auth (email) VALUES (?)') - .run(normalized); - return getAllowedEmailById(result.lastInsertRowid); + const store = await getStore(); + return (await store.google_auth.getByEmail(normalized)) ?? null; } -export function removeAllowedEmail(email) { +export async function addAllowedEmail(email) { const normalized = email.trim().toLowerCase(); - return getDb() - .prepare('DELETE FROM google_auth WHERE email = ? COLLATE NOCASE') - .run(normalized).changes > 0; + const store = await getStore(); + const id = await store.google_auth.add(normalized); + return { id, email: normalized }; +} + +export async function removeAllowedEmail(email) { + const row = await getAllowedEmail(email); + if (!row) return false; + const store = await getStore(); + await store.google_auth.del(row.id); + return true; } diff --git a/src/db/mssql/categories.js b/src/db/mssql/categories.js deleted file mode 100644 index 4df122e..0000000 --- a/src/db/mssql/categories.js +++ /dev/null @@ -1,28 +0,0 @@ -export const LOAD_KAT_MAP = ` - SELECT kKategorie, kOberKategorie - FROM dbo.tkategorie -`; - -export const LOAD_CATEGORIES = ` - SELECT - kKategorie, - kOberKategorie, - nSort, - ( - SELECT cName - FROM dbo.tKategorieSprache - WHERE kSprache = 1 - AND kPlattform = 1 - AND kShop = 0 - AND dbo.tkategorie.kKategorie = dbo.tKategorieSprache.kKategorie - ) AS cName - FROM dbo.tkategorie - ORDER BY nSort, cName -`; - -export const CATEGORY_ARTICLE_COUNTS = ` - SELECT ka.kKategorie, COUNT(*) AS count - FROM tkategorieartikel ka - INNER JOIN tArtikel a ON a.kArtikel = ka.kArtikel AND a.cAktiv = 'Y' - GROUP BY ka.kKategorie -`; diff --git a/src/db/mssql/getArticle.js b/src/db/mssql/getArticle.js deleted file mode 100644 index ac0dae7..0000000 --- a/src/db/mssql/getArticle.js +++ /dev/null @@ -1,248 +0,0 @@ -export const FIND_ARTICLE = ` - SELECT TOP 1 - tArtikelBeschreibung.cBeschreibung, - tArtikelBeschreibung.cKurzBeschreibung, - b.kVersandklasse, - (SELECT cName FROM dbo.tVersandklasse WHERE tVersandklasse.kVersandklasse = b.kVersandklasse) AS tVersandklassecName, - nIstVater, - kStueckliste, - kVaterArtikel, - b.kEigenschaftKombi, - b.kArtikel, - fUVP, - fGewicht, - tHersteller.cName AS cHersteller, - cBarcode, - fBestand, - b.cArtNr, - b.cAktiv, - tArtikelBeschreibung.cName, - b.fVKNEtto, - b.kSteuerklasse, - nVPE, - kMassEinheit, - fMassMenge, - kGrundPreisEinheit, - cAnmerkung, - tEigenschaftSprache.kEigenschaft, - tEigenschaftSprache.cName AS cEigenschaftName, - tEigenschaftWertSprache.kEigenschaftWert, - tEigenschaftWertSprache.cName AS cEigenschaftWertName, - ( - SELECT c.cArtNr - FROM tArtikel c - WHERE c.kArtikel = ( - SELECT tArtikel_kArtikel - FROM tliefartikel - WHERE cLiefArtNr = b.cArtNr AND tlieferant_klieferant = 96 - ) - ) AS cLiefArtNr96 - FROM tArtikelBeschreibung - INNER JOIN tArtikel b ON b.kArtikel = tArtikelBeschreibung.kArtikel - LEFT JOIN dbo.vLagerbestandProLager ON ( - dbo.vLagerbestandProLager.kArtikel = b.kArtikel - AND vLagerbestandProLager.kWarenlager = 1 - ) - LEFT JOIN tGebinde ON b.kArtikel = tGebinde.kArtikel - LEFT JOIN tHersteller ON b.kHersteller = tHersteller.kHersteller - LEFT JOIN tEigenschaftKombiWert ON b.kEigenschaftKombi = tEigenschaftKombiWert.kEigenschaftKombi - LEFT JOIN tEigenschaftSprache ON ( - tEigenschaftKombiWert.kEigenschaft = tEigenschaftSprache.kEigenschaft - AND tEigenschaftSprache.kSprache = 1 - ) - LEFT JOIN tEigenschaftWertSprache ON ( - tEigenschaftWertSprache.kEigenschaftWert = tEigenschaftKombiWert.kEigenschaftWert - AND tEigenschaftWertSprache.kSprache = 1 - ) - WHERE b.cArtNr = @target OR tGebinde.cEAN = @target OR b.cBarcode = @target -`; - -export const CHILDREN_BY_VATER = ` - SELECT - tArtikel.cArtNr, - tArtikelBeschreibung.cName, - tArtikel.kEigenschaftKombi, - tEigenschaftSprache.kEigenschaft, - tEigenschaftSprache.cName AS cEigenschaftName, - tEigenschaftWertSprache.kEigenschaftWert, - tEigenschaftWertSprache.cName AS cEigenschaftWertName - FROM tArtikelBeschreibung - INNER JOIN tArtikel ON tArtikel.kArtikel = tArtikelBeschreibung.kArtikel - LEFT JOIN tEigenschaftKombiWert ON tArtikel.kEigenschaftKombi = tEigenschaftKombiWert.kEigenschaftKombi - LEFT JOIN tEigenschaftSprache ON ( - tEigenschaftKombiWert.kEigenschaft = tEigenschaftSprache.kEigenschaft - AND tEigenschaftSprache.kSprache = 1 - ) - LEFT JOIN tEigenschaftWertSprache ON ( - tEigenschaftWertSprache.kEigenschaftWert = tEigenschaftKombiWert.kEigenschaftWert - AND tEigenschaftWertSprache.kSprache = 1 - ) - WHERE tArtikel.kVaterArtikel = @kArtikel -`; - -export const VATER_BY_ID = ` - SELECT tArtikel.cArtNr - FROM tArtikelBeschreibung - INNER JOIN tArtikel ON tArtikel.kArtikel = tArtikelBeschreibung.kArtikel - WHERE tArtikel.kArtikel = @kArtikel -`; - -export const STUECKLISTE = ` - SELECT - tArtikelBeschreibung.cName, - tArtikel.cArtNr, - tStueckliste.fAnzahl, - tArtikel.fVKNEtto, - tArtikel.kSteuerklasse, - vLagerbestandProLager.fBestand - FROM dbo.tStueckliste - INNER JOIN dbo.tArtikelBeschreibung ON tArtikelBeschreibung.kArtikel = tStueckliste.kArtikel - LEFT JOIN dbo.vLagerbestandProLager ON ( - vLagerbestandProLager.kArtikel = tStueckliste.kArtikel - AND vLagerbestandProLager.kWarenlager = 1 - ) - INNER JOIN dbo.tArtikel ON tArtikel.kArtikel = tStueckliste.kArtikel - WHERE tStueckliste.kStueckliste = @kStueckliste - ORDER BY tStueckliste.nSort -`; - -export const ANDERE_LAGER = ` - SELECT SUM(fBestand) AS fSumme - FROM dbo.vLagerbestandProLager - WHERE dbo.vLagerbestandProLager.kArtikel = @kArtikel - AND vLagerbestandProLager.kWarenlager != 1 - GROUP BY dbo.vLagerbestandProLager.kArtikel -`; - -export const EANS = ` - SELECT cEAN - FROM tGebinde - WHERE kArtikel = @kArtikel -`; - -export function categoriesQuery(katSkipClause, katLists) { - return ` - SELECT kKategorie - FROM dbo.tkategorieartikel - WHERE kArtikel = @kArtikel - AND ${katSkipClause}kKategorie NOT IN ( - SELECT kKategorie - FROM tKategorie - WHERE kOberKategorie = ${katLists} - ) - `; -} - -export const SUPPLIERS = ` - SELECT - tlieferant.kLieferant, - cFirma, - tliefartikel.cLiefArtNr, - nStandard, - fLagerbestand, - cVPEEinheit, - nVPEMenge, - fMwSt, - fEKNetto, - tShopKorb.nMenge, - tlieferant.cEmail, - tlieferant.cAnmerkung - FROM tliefartikel - INNER JOIN tlieferant ON tlieferant.kLieferant = tliefartikel.tLieferant_kLieferant - LEFT JOIN tShopKorb ON ( - tShopKorb.kLieferant = tlieferant.kLieferant - AND tliefartikel.cLiefArtNr = tShopKorb.cLiefArtNr - ) - WHERE tArtikel_kArtikel = @kArtikel - ORDER BY nStandard DESC, tlieferant.kLieferant -`; - -export const INCOMING_ORDERS = ` - SELECT - tLieferantenBestellung.kLieferant, - tLieferantenBestellungPos.kLieferantenBestellungPos, - tLieferantenBestellungPos.kLieferantenBestellung, - cEigeneBestellnummer, - tLieferantenBestellungPos.kLieferantenBestellung + 1 AS kLieferantenBestellung2, - cFirma, - tLieferantenBestellungPos.dLieferdatum, - cFremdbelegnummer, - cInternerKommentar, - fAnzahlOffen - FROM tLieferantenBestellung - INNER JOIN tlieferant ON tlieferant.kLieferant = tLieferantenBestellung.kLieferant - INNER JOIN tLieferantenBestellungPos ON ( - tLieferantenBestellungPos.kLieferantenBestellung = tLieferantenBestellung.kLieferantenBestellung - ) - WHERE fAnzahlOffen > 0 AND kArtikel = @kArtikel - ORDER BY tLieferantenBestellungPos.kLieferantenBestellung -`; - -export const OPEN_INCIDENTS = ` - SELECT cType, cNote, kIncident, cEmployeeIssuer, cEmployeeMap - FROM dbo.tIncident - WHERE kRef1 = @kArtikel - AND cState = 'new' - AND cType IN ('order_count', 'order_label', 'order_free') -`; - -export const MERKMALE = ` - SELECT - tArtikelMerkmal.kMerkmal, - tArtikelMerkmal.kMerkmalWert, - tMerkmalSprache.cName, - tMerkmalWertSprache.cWert - FROM dbo.tArtikelMerkmal - INNER JOIN dbo.tMerkmalSprache ON tMerkmalSprache.kMerkmal = tArtikelMerkmal.kMerkmal - INNER JOIN dbo.tMerkmalWertSprache ON tMerkmalWertSprache.kMerkmalWert = tArtikelMerkmal.kMerkmalWert - WHERE tArtikelMerkmal.kArtikel = @kArtikel - ORDER BY tArtikelMerkmal.kMerkmal -`; - -export const ATTRIBUTES = ` - SELECT - tArtikelAttribut.kAttribut, - tArtikelAttributSprache.cWertVarchar, - tArtikelAttributSprache.nWertInt, - tArtikelAttributSprache.fWertDecimal - FROM dbo.tArtikelAttribut - INNER JOIN dbo.tArtikelAttributSprache ON ( - tArtikelAttributSprache.kArtikelAttribut = tArtikelAttribut.kArtikelAttribut - AND kSprache = 0 - ) - WHERE kShop = 0 AND tArtikelAttribut.kArtikel = @kArtikel -`; - -export const STAFFEL = ` - SELECT nAnzahlAb, fNEttoPreis - FROM tPreis - INNER JOIN tPreisDetail ON tPreisDetail.kPreis = tPreis.kPreis - WHERE kArtikel = @kArtikel AND kKundenGruppe = 2 AND kShop = 0 -`; - -export const EXTRA_LAGER = ` - SELECT nMenge - FROM tExtraLager - WHERE kArtikel = @kArtikel AND nLager = @nLager -`; - -export const SHOP_SUM = ` - SELECT SUM(kShop) AS kShop - FROM tArtikelShop - WHERE kArtikel = @kArtikel -`; - -export const HERSTELLER_GPSR = ` - SELECT - tHersteller.cBeschreibung AS cHerstellerGPSR, - b.bVorschauBild AS herstellerBild - FROM tArtikel - INNER JOIN tHersteller ON tArtikel.kHersteller = tHersteller.kHersteller - LEFT JOIN tHerstellerBildPlattform hbp ON ( - hbp.kHersteller = tHersteller.kHersteller - AND hbp.kPlattform = 1 - AND hbp.kShop = 0 - ) - LEFT JOIN tBild b ON b.kBild = hbp.kBild - WHERE tArtikel.kArtikel = @kArtikel -`; diff --git a/src/db/mssql/searchArticles.js b/src/db/mssql/searchArticles.js deleted file mode 100644 index 1dc9b54..0000000 --- a/src/db/mssql/searchArticles.js +++ /dev/null @@ -1,37 +0,0 @@ -export const SELECT_COLUMNS = ` - (SELECT STRING_AGG(tk1.kKategorie, ',') - FROM dbo.tkategorieartikel AS tk1 - WHERE tk1.kArtikel = tArtikel.kArtikel - GROUP BY tk1.kArtikel) AS katList, - MIN(fVerfuegbar) AS fVerfuegbar, - tArtikel.kArtikel, - MIN(fBestand) AS fBestand, - MIN(tArtikel.cArtNr) AS cArtNr, - MIN(tArtikelBeschreibung.cName) AS cName, - MIN(tArtikel.fVKNEtto) AS fVKNEtto, - MIN(tArtikel.kSteuerklasse) AS kSteuerklasse -`; - -export const FROM_JOINS = ` - FROM tArtikelBeschreibung - INNER JOIN tArtikel ON tArtikel.kArtikel = tArtikelBeschreibung.kArtikel - LEFT JOIN tLiefArtikel ON tArtikel.kArtikel = tLiefArtikel.tArtikel_kArtikel - LEFT JOIN tGebinde ON tArtikel.kArtikel = tGebinde.kArtikel - LEFT JOIN dbo.tlagerbestand ON dbo.tlagerbestand.kArtikel = tArtikel.kArtikel - LEFT JOIN dbo.vLagerbestandProLager ON ( - dbo.vLagerbestandProLager.kArtikel = tArtikel.kArtikel - AND vLagerbestandProLager.kWarenlager = 1 - ) -`; - -export const GROUP_ORDER = ` - GROUP BY tArtikel.kArtikel - ORDER BY MIN(tArtikelBeschreibung.cName) -`; - -export const CATEGORY_JOIN = ` - INNER JOIN dbo.tkategorieartikel ON ( - tkategorieartikel.kArtikel = tArtikel.kArtikel - AND ({categoryFilter}) - ) -`; diff --git a/src/db/mssql/steuer.js b/src/db/mssql/steuer.js deleted file mode 100644 index 594392b..0000000 --- a/src/db/mssql/steuer.js +++ /dev/null @@ -1,24 +0,0 @@ -export const LOAD_STEUERSATZ = ` - SELECT - kSteuerklasse, - fSteuersatz - FROM dbo.tSteuersatz - WHERE kSteuerzone IN ( - SELECT kSteuerzone FROM dbo.tSteuerzone WHERE cName = @steuerZone - ) - ORDER BY kSteuerklasse -`; - -export const LOAD_STEUERKLASSEN = ` - SELECT - sk.kSteuerklasse, - sk.cName, - sk.nStandard, - sz.fSteuersatz - FROM dbo.tSteuerklasse sk - INNER JOIN dbo.tSteuersatz sz ON sz.kSteuerklasse = sk.kSteuerklasse - WHERE sz.kSteuerzone IN ( - SELECT kSteuerzone FROM dbo.tSteuerzone WHERE cName = @steuerZone - ) - ORDER BY sk.kSteuerklasse -`; diff --git a/src/db/mssqlClient.js b/src/db/store.js similarity index 59% rename from src/db/mssqlClient.js rename to src/db/store.js index 33f8eba..668fc34 100644 --- a/src/db/mssqlClient.js +++ b/src/db/store.js @@ -1,7 +1,9 @@ import sql from 'mssql'; +import LibDashStore from 'libdashstore'; import env from '../config/env.js'; let pool; +let store; let connecting; function getMssqlConfig() { @@ -29,36 +31,36 @@ function getMssqlConfig() { }; } -export async function getPool() { - if (pool) { - return pool; +/** + * Returns the initialized LibDashStore singleton. + * @returns {Promise>} + */ +export async function getStore() { + if (store) { + return store; } if (!connecting) { - connecting = new sql.ConnectionPool(getMssqlConfig()) - .connect() - .then((connectedPool) => { - pool = connectedPool; - return pool; - }) - .finally(() => { - connecting = undefined; - }); + connecting = (async () => { + pool = await new sql.ConnectionPool(getMssqlConfig()).connect(); + store = await new LibDashStore(pool); + return store; + })().finally(() => { + connecting = undefined; + }); } return connecting; } export async function ensureConnection() { - const activePool = await getPool(); - await activePool.request().query('SELECT 1 AS ok'); + await getStore(); } -export async function closePool() { +export async function closeStore() { if (pool) { await pool.close(); pool = undefined; + store = undefined; } } - -export { sql }; diff --git a/src/index.js b/src/index.js index 0925126..f5b5185 100644 --- a/src/index.js +++ b/src/index.js @@ -1,13 +1,11 @@ import './config/env.js'; import { startApi, stopApi } from './api/index.js'; -import { closeDb } from './db/client.js'; -import { closePool } from './db/mssqlClient.js'; +import { closeStore } from './db/store.js'; async function shutdown() { await stopApi(); try { - closeDb(); - await closePool(); + await closeStore(); } catch (error) { console.error('Failed to close database:', error); process.exitCode = 1; diff --git a/src/services/article/enrichSupplier.js b/src/services/article/enrichSupplier.js deleted file mode 100644 index 1535acb..0000000 --- a/src/services/article/enrichSupplier.js +++ /dev/null @@ -1,15 +0,0 @@ -export function enrichSupplierAnmerkungRow(row) { - const enriched = { ...row }; - - if (typeof row.cAnmerkung === 'string' && row.cAnmerkung.trim().startsWith('{')) { - try { - enriched.anmerkung = JSON.parse(row.cAnmerkung); - } catch { - enriched.anmerkung = row.cAnmerkung; - } - } else if (row.cAnmerkung != null) { - enriched.anmerkung = row.cAnmerkung; - } - - return enriched; -} diff --git a/src/services/article/getArticle.js b/src/services/article/getArticle.js deleted file mode 100644 index 33c08e3..0000000 --- a/src/services/article/getArticle.js +++ /dev/null @@ -1,193 +0,0 @@ -import env from '../../config/env.js'; -import { getPool } from '../../db/mssqlClient.js'; -import { enrichSupplierAnmerkungRow } from './enrichSupplier.js'; -import { bruttoFromNetto, loadSteuerklassen } from '../misc/getSteuer.js'; -import * as Q from '../../db/mssql/getArticle.js'; - -async function query(pool, sql, inputs = {}) { - const request = pool.request(); - for (const [name, value] of Object.entries(inputs)) { - request.input(name, value); - } - const result = await request.query(sql); - return result.recordset; -} - -function buildKatSkipClause() { - if (!env.article.katSkip.length) { - return ''; - } - return env.article.katSkip.map((id) => `kKategorie != ${id}`).join(' AND ') + ' AND '; -} - -async function findArticle(pool, target) { - const rows = await query(pool, Q.FIND_ARTICLE, { target }); - return rows[0] ?? null; -} - -async function loadChildren(pool, kArtikel) { - return query(pool, Q.CHILDREN_BY_VATER, { kArtikel }); -} - -async function loadVater(pool, kVaterArtikel) { - const rows = await query(pool, Q.VATER_BY_ID, { kArtikel: kVaterArtikel }); - return rows[0] ?? null; -} - -async function loadStueckliste(pool, kStueckliste) { - return query(pool, Q.STUECKLISTE, { kStueckliste }); -} - -async function loadWarehouseExtras(pool, kArtikel) { - const [fAndereLager, EANs, KATs] = await Promise.all([ - query(pool, Q.ANDERE_LAGER, { kArtikel }), - query(pool, Q.EANS, { kArtikel }), - query(pool, Q.categoriesQuery(buildKatSkipClause(), env.article.katLists), { kArtikel }), - ]); - - return { fAndereLager, EANs, KATs }; -} - -async function loadSuppliers(pool, kArtikel) { - const rows = await query(pool, Q.SUPPLIERS, { kArtikel }); - return rows.map(enrichSupplierAnmerkungRow); -} - -async function loadIncomingOrders(pool, kArtikel) { - return query(pool, Q.INCOMING_ORDERS, { kArtikel }); -} - -async function loadOpenIncidents(pool, kArtikel) { - return query(pool, Q.OPEN_INCIDENTS, { kArtikel }); -} - -async function loadMerkmale(pool, kArtikel) { - return query(pool, Q.MERKMALE, { kArtikel }); -} - -async function loadAttributes(pool, kArtikel) { - return query(pool, Q.ATTRIBUTES, { kArtikel }); -} - -async function loadStaffel(pool, kArtikel, kSteuerklasse) { - const rows = await query(pool, Q.STAFFEL, { kArtikel }); - const steuerklassen = await loadSteuerklassen(); - return rows.map((item) => ({ - menge: item.nAnzahlAb.toString(), - fVKBrutto: bruttoFromNetto(item.fNEttoPreis, kSteuerklasse, steuerklassen)?.toFixed(2), - })); -} - -async function loadExtraLager(pool, kArtikel, nLager) { - const rows = await query(pool, Q.EXTRA_LAGER, { kArtikel, nLager }); - return rows[0]?.nMenge; -} - -async function loadShopSum(pool, kArtikel) { - const rows = await query(pool, Q.SHOP_SUM, { kArtikel }); - return rows[0]?.kShop; -} - -async function loadHerstellerGpsr(pool, kArtikel) { - const rows = await query(pool, Q.HERSTELLER_GPSR, { kArtikel }); - const row = rows[0]; - if (!row) { - return { cHerstellerGPSR: null, herstellerBild: null }; - } - - const herstellerBild = row.herstellerBild - ? Buffer.from(row.herstellerBild).toString('base64') - : null; - - return { - cHerstellerGPSR: row.cHerstellerGPSR ?? null, - herstellerBild, - }; -} - -async function enrichArticle(pool, article) { - const { kArtikel, kSteuerklasse, nIstVater, kVaterArtikel, kStueckliste } = article; - - const parallel = []; - - if (nIstVater === 1) { - parallel.push( - loadChildren(pool, kArtikel).then((children) => { - article.children = children; - }) - ); - } - - if (kVaterArtikel) { - parallel.push( - loadVater(pool, kVaterArtikel).then((vater) => { - article.vater = vater; - }) - ); - } - - if (kStueckliste !== 0) { - parallel.push( - loadStueckliste(pool, kStueckliste).then((liste) => { - article.liste = liste; - }) - ); - } - - parallel.push( - loadWarehouseExtras(pool, kArtikel).then(({ fAndereLager, EANs, KATs }) => { - article.fAndereLager = fAndereLager; - article.EANs = EANs; - article.KATs = KATs; - }), - loadSuppliers(pool, kArtikel).then((lief) => { - article.lief = lief; - }), - loadIncomingOrders(pool, kArtikel).then((incoming) => { - article.incoming = incoming; - }), - loadOpenIncidents(pool, kArtikel).then((orders) => { - article.orders = orders; - }), - loadMerkmale(pool, kArtikel).then((merkmale) => { - article.merkmale = merkmale; - }), - loadAttributes(pool, kArtikel).then((attribute) => { - article.attribute = attribute; - }), - loadStaffel(pool, kArtikel, kSteuerklasse).then((staffel) => { - article.staffel = staffel; - }), - loadExtraLager(pool, kArtikel, 0).then((garage) => { - article.garage = garage; - }), - loadExtraLager(pool, kArtikel, 1).then((keller) => { - article.keller = keller; - }), - loadShopSum(pool, kArtikel).then((shops) => { - article.shops = shops; - }), - loadHerstellerGpsr(pool, kArtikel).then(({ cHerstellerGPSR, herstellerBild }) => { - article.cHerstellerGPSR = cHerstellerGPSR; - article.herstellerBild = herstellerBild; - }) - ); - - await Promise.all(parallel); - return article; -} - -export async function getArticle(target) { - const trimmed = target?.trim(); - if (!trimmed) { - return null; - } - - const pool = await getPool(); - const article = await findArticle(pool, trimmed); - if (!article) { - return null; - } - - return enrichArticle(pool, article); -} diff --git a/src/services/article/searchArticles.js b/src/services/article/searchArticles.js deleted file mode 100644 index 7085371..0000000 --- a/src/services/article/searchArticles.js +++ /dev/null @@ -1,229 +0,0 @@ -import { getPool } from '../../db/mssqlClient.js'; -import { LOAD_CATEGORIES } from '../../db/mssql/categories.js'; -import * as Q from '../../db/mssql/searchArticles.js'; -import { bruttoFromNetto, loadSteuersatz } from '../misc/getSteuer.js'; - -let categoryCache; - -export function parseFilter(raw) { - if (raw === undefined || raw === null || raw === '') { - return ['']; - } - const parts = String(raw).split(',').map((s) => s.trim()).filter(Boolean); - return parts.length ? parts : ['']; -} - -function getUpPath(kat, parentMap) { - const list = [kat]; - if (parentMap[kat]) { - for (const item of getUpPath(parentMap[kat], parentMap)) { - list.push(item); - } - } - return list; -} - -async function loadCategoryCache(pool) { - if (categoryCache) { - return categoryCache; - } - - const result = await pool.request().query(LOAD_CATEGORIES); - const byId = {}; - const parentMap = {}; - for (const item of result.recordset) { - byId[item.kKategorie] = { - name: item.cName ?? null, - parent: item.kOberKategorie, - }; - parentMap[item.kKategorie] = item.kOberKategorie; - } - categoryCache = { byId, parentMap }; - return categoryCache; -} - -function buildKatAgg(recordset, parentMap) { - const counts = {}; - - for (const item of recordset) { - if (!item.katList) { - continue; - } - for (const kat of item.katList.split(',')) { - for (const katpart of getUpPath(kat, parentMap)) { - if (!counts[katpart]) { - counts[katpart] = 0; - } - counts[katpart]++; - } - } - } - - return counts; -} - -function shapeCategories(counts, byId) { - const categories = {}; - - for (const [id, count] of Object.entries(counts)) { - const meta = byId[id] ?? {}; - categories[id] = { - count, - name: meta.name ?? null, - parent: meta.parent ?? null, - }; - } - - return categories; -} - -/** - * Ensure every applied filter id and its ancestors appear in categories - * (count 0 if no hits), so the frontend can render hierarchy via parent links. - */ -function ensureFilterCategories(categories, filter, byId, parentMap) { - for (const id of filter) { - if (id === '' || id === null) { - continue; - } - for (const ancestorId of getUpPath(id, parentMap)) { - const key = String(ancestorId); - if (categories[key]) { - continue; - } - const meta = byId[ancestorId] ?? byId[key] ?? {}; - categories[key] = { - count: 0, - name: meta.name ?? null, - parent: meta.parent ?? null, - }; - } - } - return categories; -} - -function shapeArticle(row, steuersaetze) { - const taxRow = steuersaetze.find((item) => item.kSteuerklasse === row.kSteuerklasse); - const brutto = bruttoFromNetto(row.fVKNEtto, row.kSteuerklasse, steuersaetze); - - return { - categories: row.katList, - available: row.fVerfuegbar, - id: row.kArtikel, - stock: row.fBestand, - sku: row.cArtNr, - name: row.cName, - price: brutto == null ? null : Math.round(brutto * 10) / 10, - tax: taxRow?.fSteuersatz ?? null, - }; -} - -function buildTargetClause(request, target) { - const parts = []; - let i = 0; - - const terms = target - .toString() - .trim() - .replace(/ {2,}/g, ' ') - .split(' ') - .filter(Boolean); - - for (const part of terms) { - request.input(`target_${i}`, `%${part}%`); - parts.push( - 'tGebinde.cEAN LIKE @target_' + i + - ' OR tLiefArtikel.cLiefArtNr LIKE @target_' + i + - ' OR tArtikelBeschreibung.cName LIKE @target_' + i + - ' OR tArtikel.cArtNr LIKE @target_' + i + - ' OR tArtikel.cBarcode LIKE @target_' + i - ); - i++; - } - - return { parts, nextIndex: i }; -} - -function buildCategoryClause(request, filter, startIndex) { - const parts = []; - let i = startIndex; - - for (const part of filter) { - request.input(`target_${i}`, part); - parts.push(`tkategorieartikel.kKategorie = @target_${i}`); - i++; - } - - return { - clause: '(' + parts.join(') OR (') + ')', - nextIndex: i, - }; -} - -function buildWhereClause(parts, filter) { - const aktivClause = filter?.[0] == '751' ? '' : " AND (tArtikel.cAktiv = 'Y')"; - if (!parts.length) { - return `1=1${aktivClause}`; - } - return '(' + parts.join(') AND (') + ')' + aktivClause; -} - -function buildQuery(filter, categoryFilter) { - const select = filter[0] === '' || filter[0] === null - ? `SELECT TOP (2000) ${Q.SELECT_COLUMNS}` - : `SELECT ${Q.SELECT_COLUMNS}`; - - const categoryJoin = categoryFilter - ? Q.CATEGORY_JOIN.replace('{categoryFilter}', categoryFilter) - : ''; - - return `${select} - ${Q.FROM_JOINS} - ${categoryJoin} - WHERE `; -} - -export async function searchArticles(target, filter = [''], offset = 0, maxCount = 10) { - const trimmed = target?.toString().trim(); - if (!trimmed) { - return { categories: {}, length: 0, recordset: [] }; - } - - const start = Math.max(0, Number(offset) || 0); - const count = Math.max(0, Number(maxCount) || 10); - - const pool = await getPool(); - const request = pool.request(); - const { parts } = buildTargetClause(request, trimmed); - const whereClause = buildWhereClause(parts, filter); - - let sql = buildQuery(filter, null) + whereClause + Q.GROUP_ORDER; - - if (filter[0] !== '' && filter[0] !== null) { - const { clause: categoryFilter } = buildCategoryClause(request, filter, parts.length); - sql = buildQuery(filter, categoryFilter) + whereClause + Q.GROUP_ORDER; - } - - const result = await request.query(sql); - const [categoryData, steuersaetze] = await Promise.all([ - loadCategoryCache(pool), - loadSteuersatz(), - ]); - const categories = ensureFilterCategories( - shapeCategories( - buildKatAgg(result.recordset, categoryData.parentMap), - categoryData.byId - ), - filter, - categoryData.byId, - categoryData.parentMap - ); - const length = result.recordset.length; - const recordset = result.recordset - .slice(start, start + count) - .map((row) => shapeArticle(row, steuersaetze)); - - console.dir({ categories, length, recordset }, { depth: null }); - - return { categories, length, recordset }; -} diff --git a/src/services/misc/getCategories.js b/src/services/misc/getCategories.js deleted file mode 100644 index 729a063..0000000 --- a/src/services/misc/getCategories.js +++ /dev/null @@ -1,77 +0,0 @@ -import { getPool } from '../../db/mssqlClient.js'; -import { - CATEGORY_ARTICLE_COUNTS, - LOAD_CATEGORIES, -} from '../../db/mssql/categories.js'; - -function sortCategories(list) { - list.sort((a, b) => { - const sortDiff = (a.nSort ?? 0) - (b.nSort ?? 0); - if (sortDiff !== 0) { - return sortDiff; - } - return String(a.cName ?? '').localeCompare(String(b.cName ?? ''), 'de'); - }); - for (const node of list) { - sortCategories(node.children); - } -} - -function buildCategoryTree(rows) { - const nodes = new Map(); - - for (const row of rows) { - nodes.set(row.kKategorie, { - kKategorie: row.kKategorie, - kOberKategorie: row.kOberKategorie, - nSort: row.nSort, - cName: row.cName ?? null, - count: row.count ?? 0, - children: [], - }); - } - - const roots = []; - - for (const node of nodes.values()) { - const parent = nodes.get(node.kOberKategorie); - if (parent && node.kOberKategorie !== node.kKategorie) { - parent.children.push(node); - } else { - roots.push(node); - } - } - - sortCategories(roots); - return roots; -} - -/** - * Loads the full JTL category list with per-category article counts and a nested tree. - * @returns {Promise<{ categories: Array<{ kKategorie: number, kOberKategorie: number, nSort: number, cName: string|null, count: number }>, tree: object[] }>} - */ -export async function getCategories() { - const pool = await getPool(); - - const [categoriesResult, countsResult] = await Promise.all([ - pool.request().query(LOAD_CATEGORIES), - pool.request().query(CATEGORY_ARTICLE_COUNTS), - ]); - - const countByCategory = new Map( - countsResult.recordset.map((row) => [row.kKategorie, row.count]) - ); - - const categories = categoriesResult.recordset.map((row) => ({ - kKategorie: row.kKategorie, - kOberKategorie: row.kOberKategorie, - nSort: row.nSort, - cName: row.cName ?? null, - count: countByCategory.get(row.kKategorie) ?? 0, - })); - - return { - categories, - tree: buildCategoryTree(categories), - }; -} diff --git a/src/services/misc/getSteuer.js b/src/services/misc/getSteuer.js deleted file mode 100644 index 08dc033..0000000 --- a/src/services/misc/getSteuer.js +++ /dev/null @@ -1,63 +0,0 @@ -import { getPool } from '../../db/mssqlClient.js'; -import { LOAD_STEUERSATZ, LOAD_STEUERKLASSEN } from '../../db/mssql/steuer.js'; -import env from '../../config/env.js'; - -let steuersatzCache; -let steuerklassenCache; - -/** - * Loads VAT rates per tax class for the configured steuer zone (`tSteuersatz` only). - * Results are cached for the process lifetime. - * @returns {Promise>} - */ -export async function loadSteuersatz() { - if (steuersatzCache) { - return steuersatzCache; - } - - const pool = await getPool(); - const result = await pool.request() - .input('steuerZone', env.steuerZone) - .query(LOAD_STEUERSATZ); - steuersatzCache = result.recordset.map((row) => ({ - kSteuerklasse: row.kSteuerklasse, - fSteuersatz: row.fSteuersatz, - })); - return steuersatzCache; -} - -/** - * Loads tax classes with display metadata for the configured steuer zone. - * Results are cached for the process lifetime. - * @returns {Promise>} - */ -export async function loadSteuerklassen() { - if (steuerklassenCache) { - return steuerklassenCache; - } - - const pool = await getPool(); - const result = await pool.request() - .input('steuerZone', env.steuerZone) - .query(LOAD_STEUERKLASSEN); - steuerklassenCache = result.recordset.map((row) => ({ - kSteuerklasse: row.kSteuerklasse, - cName: row.cName, - nStandard: row.nStandard, - fSteuersatz: row.fSteuersatz, - })); - return steuerklassenCache; -} - -export function bruttoFromNetto(netto, kSteuerklasse, steuersaetze) { - const row = steuersaetze.find((item) => item.kSteuerklasse === kSteuerklasse); - if (row == null || netto == null) { - return null; - } - return netto * (1 + row.fSteuersatz / 100); -} - -export async function getSteuer() { - const steuerklassen = await loadSteuerklassen(); - return { steuerklassen }; -}