From 92444ff38c0ab92c9c8cb3674146fd2af3c8299f Mon Sep 17 00:00:00 2001 From: "manuel.sowada" Date: Wed, 22 Apr 2026 11:55:23 +0200 Subject: [PATCH] initial files --- .gitignore | 8 + license_internal.txt | 4 + package-lock.json | 4937 ++++++++++++++++++++++ package.json | 50 + public/helpers/comparisonHelpers.js | 72 + public/helpers/dateHelpers.js | 6 + public/helpers/fileHelper.js | 24 + public/helpers/iterationHelpers.js | 145 + public/images/app.png | Bin 0 -> 729 bytes public/images/arrow_left.png | Bin 0 -> 2885 bytes public/images/arrow_right.png | Bin 0 -> 2890 bytes public/images/brush.png | Bin 0 -> 2691 bytes public/images/cursor_dark.png | Bin 0 -> 957 bytes public/images/cursor_light.png | Bin 0 -> 5590 bytes public/images/cursor_modern.png | Bin 0 -> 1084 bytes public/images/cursor_pointer_dark.png | Bin 0 -> 4879 bytes public/images/cursor_pointer_light.png | Bin 0 -> 5781 bytes public/images/cursor_pointer_modern.png | Bin 0 -> 4749 bytes public/images/eventlog.ico | Bin 0 -> 19518 bytes public/images/folder.png | Bin 0 -> 9497 bytes public/images/help.png | Bin 0 -> 3310 bytes public/images/notifybubble.png | Bin 0 -> 3469 bytes public/images/plugins.png | Bin 0 -> 2674 bytes public/images/serverinfo.png | Bin 0 -> 914 bytes public/images/tutorial.png | Bin 0 -> 4074 bytes public/javascript/JSON.js | 361 ++ public/javascript/contextMenu.js | 244 ++ public/javascript/customModal.js | 259 ++ public/javascript/loadOnce.js | 97 + public/javascript/main.js | 1494 +++++++ public/javascript/notifyBubble.js | 157 + public/javascript/os.js | 1081 +++++ public/javascript/pluginAPI.js | 92 + public/javascript/requiredFields.js | 89 + public/javascript/tableFilter.js | 450 ++ public/javascript/tutorial.js | 354 ++ public/styles/colors.css | 131 + public/styles/contextMenu.css | 79 + public/styles/default.css | 434 ++ public/styles/jsonTree.css | 79 + public/styles/os.css | 217 + public/styles/table.css | 86 + public/styles/userNotification.css | 67 + public/views/desktop.hbs | 153 + public/views/eventlog.hbs | 127 + public/views/help/Hilfe.html | 36 + public/views/integrated/development.hbs | 11 + public/views/integrated/help.hbs | 106 + public/views/integrated/serverconfig.hbs | 35 + public/views/integrated/serverinfo.hbs | 101 + public/views/integrated/styleconfig.hbs | 30 + public/views/integrated/usersettings.hbs | 113 + public/views/layouts/default.hbs | 9 + public/views/login.hbs | 187 + public/views/partials/child.hbs | 16 + public/views/partials/window.hbs | 19 + public/views/plugindashboard.hbs | 242 ++ server.js | 253 ++ skeleton.txt | 138 + src/models/authenticationModel.js | 84 + src/models/eventlogModel.js | 51 + src/models/eventlogView.js | 34 + src/models/integratedStartmenuItems.js | 108 + src/models/notifyTrayModel.js | 40 + src/models/notifyTrayObjectsModel.js | 39 + src/models/notifyTrayView.js | 70 + src/models/pluginModel.js | 24 + src/models/releasenotes.json | 58 + src/routes/adminRoutes.js | 175 + src/routes/indexRoutes.js | 125 + src/routes/loginRoutes.js | 79 + src/services/activeDirectoryManager.js | 258 ++ src/services/authenticationManager.js | 172 + src/services/eventManager.js | 159 + src/services/fileSystemManager.js | 187 + src/services/hotReload.js | 377 ++ src/services/identityManager.js | 302 ++ src/services/notifyTrayManager.js | 137 + src/services/pluginManager.js | 497 +++ src/services/renderWindow.js | 85 + src/services/socketManager.js | 336 ++ src/services/sqlManager.js | 139 + src/sockets/adminSocket.js | 14 + src/sockets/mainSocket.js | 25 + utils.js | 156 + 85 files changed, 16324 insertions(+) create mode 100644 .gitignore create mode 100644 license_internal.txt create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/helpers/comparisonHelpers.js create mode 100644 public/helpers/dateHelpers.js create mode 100644 public/helpers/fileHelper.js create mode 100644 public/helpers/iterationHelpers.js create mode 100644 public/images/app.png create mode 100644 public/images/arrow_left.png create mode 100644 public/images/arrow_right.png create mode 100644 public/images/brush.png create mode 100644 public/images/cursor_dark.png create mode 100644 public/images/cursor_light.png create mode 100644 public/images/cursor_modern.png create mode 100644 public/images/cursor_pointer_dark.png create mode 100644 public/images/cursor_pointer_light.png create mode 100644 public/images/cursor_pointer_modern.png create mode 100644 public/images/eventlog.ico create mode 100644 public/images/folder.png create mode 100644 public/images/help.png create mode 100644 public/images/notifybubble.png create mode 100644 public/images/plugins.png create mode 100644 public/images/serverinfo.png create mode 100644 public/images/tutorial.png create mode 100644 public/javascript/JSON.js create mode 100644 public/javascript/contextMenu.js create mode 100644 public/javascript/customModal.js create mode 100644 public/javascript/loadOnce.js create mode 100644 public/javascript/main.js create mode 100644 public/javascript/notifyBubble.js create mode 100644 public/javascript/os.js create mode 100644 public/javascript/pluginAPI.js create mode 100644 public/javascript/requiredFields.js create mode 100644 public/javascript/tableFilter.js create mode 100644 public/javascript/tutorial.js create mode 100644 public/styles/colors.css create mode 100644 public/styles/contextMenu.css create mode 100644 public/styles/default.css create mode 100644 public/styles/jsonTree.css create mode 100644 public/styles/os.css create mode 100644 public/styles/table.css create mode 100644 public/styles/userNotification.css create mode 100644 public/views/desktop.hbs create mode 100644 public/views/eventlog.hbs create mode 100644 public/views/help/Hilfe.html create mode 100644 public/views/integrated/development.hbs create mode 100644 public/views/integrated/help.hbs create mode 100644 public/views/integrated/serverconfig.hbs create mode 100644 public/views/integrated/serverinfo.hbs create mode 100644 public/views/integrated/styleconfig.hbs create mode 100644 public/views/integrated/usersettings.hbs create mode 100644 public/views/layouts/default.hbs create mode 100644 public/views/login.hbs create mode 100644 public/views/partials/child.hbs create mode 100644 public/views/partials/window.hbs create mode 100644 public/views/plugindashboard.hbs create mode 100644 server.js create mode 100644 skeleton.txt create mode 100644 src/models/authenticationModel.js create mode 100644 src/models/eventlogModel.js create mode 100644 src/models/eventlogView.js create mode 100644 src/models/integratedStartmenuItems.js create mode 100644 src/models/notifyTrayModel.js create mode 100644 src/models/notifyTrayObjectsModel.js create mode 100644 src/models/notifyTrayView.js create mode 100644 src/models/pluginModel.js create mode 100644 src/models/releasenotes.json create mode 100644 src/routes/adminRoutes.js create mode 100644 src/routes/indexRoutes.js create mode 100644 src/routes/loginRoutes.js create mode 100644 src/services/activeDirectoryManager.js create mode 100644 src/services/authenticationManager.js create mode 100644 src/services/eventManager.js create mode 100644 src/services/fileSystemManager.js create mode 100644 src/services/hotReload.js create mode 100644 src/services/identityManager.js create mode 100644 src/services/notifyTrayManager.js create mode 100644 src/services/pluginManager.js create mode 100644 src/services/renderWindow.js create mode 100644 src/services/socketManager.js create mode 100644 src/services/sqlManager.js create mode 100644 src/sockets/adminSocket.js create mode 100644 src/sockets/mainSocket.js create mode 100644 utils.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6228ce3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +plugins/ +secure/ +configuration.json +stylesheet.json +.npmrc +radix_os_*.png +radix_os_icon.ico \ No newline at end of file diff --git a/license_internal.txt b/license_internal.txt new file mode 100644 index 0000000..b08f7b6 --- /dev/null +++ b/license_internal.txt @@ -0,0 +1,4 @@ +© 2025 Grünflächenamt | Manuel Sowada + +Diese Software ist ausschließlich für den internen dienstlichen Gebrauch durch Mitarbeiter des Grünflächenamtes vorgesehen. +Weitergabe, Veröffentlichung oder private Nutzung ist ohne ausdrückliche Genehmigung untersagt. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a924858 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4937 @@ +{ + "name": "radixos", + "version": "0.9", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "radixos", + "version": "0.9", + "license": "UNLICENSED", + "dependencies": { + "activedirectory2": "^2.2.0", + "bcryptjs": "^3.0.2", + "body-parser": "^2.2.0", + "child_process": "^1.0.2", + "chokidar": "^4.0.3", + "cookie-parser": "^1.4.7", + "express": "^5.1.0", + "express-handlebars": "^8.0.3", + "fs": "^0.0.1-security", + "fs-extra": "^11.3.2", + "https": "^1.0.0", + "jsonwebtoken": "^9.0.2", + "ldapjs": "^3.0.7", + "module-alias": "^2.2.3", + "multer": "^2.0.2", + "net": "^1.0.2", + "npm": "^11.6.4", + "oracledb": "^6.10.0", + "os": "^0.1.2", + "p-limit": "^7.2.0", + "path": "^0.12.7", + "sequelize": "^6.37.7", + "serve-favicon": "^2.5.1", + "socket.io": "^4.8.1", + "tedious": "^18.6.1" + } + }, + "node_modules/@azure-rest/core-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", + "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz", + "integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.1.tgz", + "integrity": "sha512-UVZlVLfLyz6g3Hy7GNDpooMQonUygH7ghdiSASOOHy97fKj/mPLqgDX7aidOijn+sCMU+WU8NjlPlNTgnvbcGA==", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", + "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.26.0.tgz", + "integrity": "sha512-Ie3SZ4IMrf9lSwWVzzJrhTPE+g9+QDUfeor1LKMBQzcblp+3J/U1G8hMpNSfLL7eA5F/DjjPXkATJ5JRUdDJLA==", + "dependencies": { + "@azure/msal-common": "15.13.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.13.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.1.tgz", + "integrity": "sha512-vQYQcG4J43UWgo1lj7LcmdsGUKWYo28RfEvDQAEMmQIMjSFufvb+pS0FJ3KXmrPmnWlt1vHDl3oip6mIDUQ4uA==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.1.tgz", + "integrity": "sha512-HszfqoC+i2C9+BRDQfuNUGp15Re7menIhCEbFCQ49D3KaqEDrgZIgQ8zSct4T59jWeUIL9N/Dwiv4o2VueTdqQ==", + "dependencies": { + "@azure/msal-common": "15.13.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@js-joda/core": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.5.tgz", + "integrity": "sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ==" + }, + "node_modules/@ldapjs/asn1": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz", + "integrity": "sha512-G9+DkEOirNgdPmD0I8nu57ygQJKOOgFEMKknEuQvIHbGLwP3ny1mY+OTUYLCbCaGJP4sox5eYgBJRuSUpnAddA==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md" + }, + "node_modules/@ldapjs/attribute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ldapjs/attribute/-/attribute-1.0.0.tgz", + "integrity": "sha512-ptMl2d/5xJ0q+RgmnqOi3Zgwk/TMJYG7dYMC0Keko+yZU6n+oFM59MjQOUht5pxJeS4FWrImhu/LebX24vJNRQ==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "2.0.0", + "@ldapjs/protocol": "^1.2.1", + "process-warning": "^2.1.0" + } + }, + "node_modules/@ldapjs/change": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ldapjs/change/-/change-1.0.0.tgz", + "integrity": "sha512-EOQNFH1RIku3M1s0OAJOzGfAohuFYXFY4s73wOhRm4KFGhmQQ7MChOh2YtYu9Kwgvuq1B0xKciXVzHCGkB5V+Q==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "2.0.0", + "@ldapjs/attribute": "1.0.0" + } + }, + "node_modules/@ldapjs/controls": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ldapjs/controls/-/controls-2.1.0.tgz", + "integrity": "sha512-2pFdD1yRC9V9hXfAWvCCO2RRWK9OdIEcJIos/9cCVP9O4k72BY1bLDQQ4KpUoJnl4y/JoD4iFgM+YWT3IfITWw==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "^1.2.0", + "@ldapjs/protocol": "^1.2.1" + } + }, + "node_modules/@ldapjs/controls/node_modules/@ldapjs/asn1": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-1.2.0.tgz", + "integrity": "sha512-KX/qQJ2xxzvO2/WOvr1UdQ+8P5dVvuOLk/C9b1bIkXxZss8BaR28njXdPgFCpj5aHaf1t8PmuVnea+N9YG9YMw==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md" + }, + "node_modules/@ldapjs/dn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ldapjs/dn/-/dn-1.1.0.tgz", + "integrity": "sha512-R72zH5ZeBj/Fujf/yBu78YzpJjJXG46YHFo5E4W1EqfNpo1UsVPqdLrRMXeKIsJT3x9dJVIfR6OpzgINlKpi0A==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "2.0.0", + "process-warning": "^2.1.0" + } + }, + "node_modules/@ldapjs/filter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@ldapjs/filter/-/filter-2.1.1.tgz", + "integrity": "sha512-TwPK5eEgNdUO1ABPBUQabcZ+h9heDORE4V9WNZqCtYLKc06+6+UAJ3IAbr0L0bYTnkkWC/JEQD2F+zAFsuikNw==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "2.0.0", + "@ldapjs/protocol": "^1.2.1", + "process-warning": "^2.1.0" + } + }, + "node_modules/@ldapjs/messages": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ldapjs/messages/-/messages-1.3.0.tgz", + "integrity": "sha512-K7xZpXJ21bj92jS35wtRbdcNrwmxAtPwy4myeh9duy/eR3xQKvikVycbdWVzkYEAVE5Ce520VXNOwCHjomjCZw==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "^2.0.0", + "@ldapjs/attribute": "^1.0.0", + "@ldapjs/change": "^1.0.0", + "@ldapjs/controls": "^2.1.0", + "@ldapjs/dn": "^1.1.0", + "@ldapjs/filter": "^2.1.1", + "@ldapjs/protocol": "^1.2.1", + "process-warning": "^2.2.0" + } + }, + "node_modules/@ldapjs/protocol": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz", + "integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "node_modules/@types/node": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.22.tgz", + "integrity": "sha512-/FFhJpfCLAPwAcN3mFycNUa77ddnr8jTgF5VmSNetaemWB2cIlfCA9t0YTM3JAT0wOcv8D4tjPo7pkDhK3EJIg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/validator": { + "version": "13.15.4", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.4.tgz", + "integrity": "sha512-LSFfpSnJJY9wbC0LQxgvfb+ynbHftFo0tMsFOl/J4wexLnYMmDSPaj2ZyDv3TkfL1UePxPrxOWJfbiRS8mQv7A==" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.1.tgz", + "integrity": "sha512-SnbaqayTVFEA6/tYumdF0UmybY0KHyKwGPBXnyckFlrrKdhWFrL3a2HIPXHjht5ZOElKGcXfD2D63P36btb+ww==", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/activedirectory2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/activedirectory2/-/activedirectory2-2.2.0.tgz", + "integrity": "sha512-uGbw74xttFG6hgocU8T1a0oDofLsyTp44BPTn42JN5C2QlyO5kRl2E7ZoUdfpFzV+yxhaQTKI+8QqRB5HONYvA==", + "deprecated": "Decomissioned.", + "dependencies": { + "abstract-logging": "^2.0.0", + "async": "^3.1.0", + "ldapjs": "^2.3.3", + "merge-options": "^2.0.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/activedirectory2/node_modules/ldapjs": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz", + "integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "abstract-logging": "^2.0.0", + "asn1": "^0.2.4", + "assert-plus": "^1.0.0", + "backoff": "^2.5.0", + "ldap-filter": "^0.3.3", + "once": "^1.4.0", + "vasync": "^2.2.0", + "verror": "^1.8.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", + "dependencies": { + "precond": "0.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/bl": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.4.tgz", + "integrity": "sha512-ZV/9asSuknOExbM/zPPA8z00lc1ihPKWaStHkkQrxHNeYx+yY+TmF+v80dpv2G0mv3HVXBu7ryoAsxbFFhf4eg==", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "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", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "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==" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/child_process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", + "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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==", + "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==" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/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==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dottie": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz", + "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-handlebars": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-8.0.3.tgz", + "integrity": "sha512-uzvrS2HRlhFNmq2dZb6EJahu2tg7trV9wuYOJrkIbstemozwOvNRa0idkHBjy62nPu6wao76AlhJ69YBIHfkEA==", + "dependencies": { + "glob": "^11.0.2", + "graceful-fs": "^4.2.11", + "handlebars": "^4.7.8" + }, + "engines": { + "node": ">=22.15.0" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "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==" + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ldap-filter": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", + "integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ldapjs": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-3.0.7.tgz", + "integrity": "sha512-1ky+WrN+4CFMuoekUOv7Y1037XWdjKpu0xAPwSP+9KdvmV9PG+qOKlssDV6a+U32apwxdD3is/BZcWOYzN30cg==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "^2.0.0", + "@ldapjs/attribute": "^1.0.0", + "@ldapjs/change": "^1.0.0", + "@ldapjs/controls": "^2.1.0", + "@ldapjs/dn": "^1.1.0", + "@ldapjs/filter": "^2.1.1", + "@ldapjs/messages": "^1.3.0", + "@ldapjs/protocol": "^1.2.1", + "abstract-logging": "^2.0.1", + "assert-plus": "^1.0.0", + "backoff": "^2.5.0", + "once": "^1.4.0", + "vasync": "^2.2.1", + "verror": "^1.10.1" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-2.0.0.tgz", + "integrity": "sha512-S7xYIeWHl2ZUKF7SDeBhGg6rfv5bKxVBdk95s/I7wVF8d+hjLSztJ/B271cnUiF6CAFduEQ5Zn3HYwAjT16DlQ==", + "dependencies": { + "is-plain-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/module-alias": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==" + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "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==" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/net": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", + "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==" + }, + "node_modules/npm": { + "version": "11.12.1", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.12.1.tgz", + "integrity": "sha512-zcoUuF1kezGSAo0CqtvoLXX3mkRqzuqYdL6Y5tdo8g69NVV3CkjQ6ZBhBgB4d7vGkPcV6TcvLi3GRKPDFX+xTA==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/metavuln-calculator", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/redact", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which" + ], + "license": "Artistic-2.0", + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^9.4.2", + "@npmcli/config": "^10.8.1", + "@npmcli/fs": "^5.0.0", + "@npmcli/map-workspaces": "^5.0.3", + "@npmcli/metavuln-calculator": "^9.0.3", + "@npmcli/package-json": "^7.0.5", + "@npmcli/promise-spawn": "^9.0.1", + "@npmcli/redact": "^4.0.0", + "@npmcli/run-script": "^10.0.4", + "@sigstore/tuf": "^4.0.2", + "abbrev": "^4.0.0", + "archy": "~1.0.0", + "cacache": "^20.0.4", + "chalk": "^5.6.2", + "ci-info": "^4.4.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^13.0.6", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^9.0.2", + "ini": "^6.0.0", + "init-package-json": "^8.2.5", + "is-cidr": "^6.0.3", + "json-parse-even-better-errors": "^5.0.0", + "libnpmaccess": "^10.0.3", + "libnpmdiff": "^8.1.5", + "libnpmexec": "^10.2.5", + "libnpmfund": "^7.0.19", + "libnpmorg": "^8.0.1", + "libnpmpack": "^9.1.5", + "libnpmpublish": "^11.1.3", + "libnpmsearch": "^9.0.1", + "libnpmteam": "^8.0.2", + "libnpmversion": "^8.0.3", + "make-fetch-happen": "^15.0.5", + "minimatch": "^10.2.4", + "minipass": "^7.1.3", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^12.2.0", + "nopt": "^9.0.0", + "npm-audit-report": "^7.0.0", + "npm-install-checks": "^8.0.0", + "npm-package-arg": "^13.0.2", + "npm-pick-manifest": "^11.0.3", + "npm-profile": "^12.0.1", + "npm-registry-fetch": "^19.1.1", + "npm-user-validate": "^4.0.0", + "p-map": "^7.0.4", + "pacote": "^21.5.0", + "parse-conflict-json": "^5.0.1", + "proc-log": "^6.1.0", + "qrcode-terminal": "^0.12.0", + "read": "^5.0.1", + "semver": "^7.7.4", + "spdx-expression-parse": "^4.0.0", + "ssri": "^13.0.1", + "supports-color": "^10.2.2", + "tar": "^7.5.11", + "text-table": "~0.2.0", + "tiny-relative-date": "^2.0.2", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^7.0.2", + "which": "^6.0.1" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@gar/promise-retry": { + "version": "1.0.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "9.4.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^5.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/metavuln-calculator": "^9.0.2", + "@npmcli/name-from-folder": "^4.0.0", + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/query": "^5.0.0", + "@npmcli/redact": "^4.0.0", + "@npmcli/run-script": "^10.0.0", + "bin-links": "^6.0.0", + "cacache": "^20.0.1", + "common-ancestor-path": "^2.0.0", + "hosted-git-info": "^9.0.0", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^11.2.1", + "minimatch": "^10.0.3", + "nopt": "^9.0.0", + "npm-install-checks": "^8.0.0", + "npm-package-arg": "^13.0.0", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "pacote": "^21.0.2", + "parse-conflict-json": "^5.0.1", + "proc-log": "^6.0.0", + "proggy": "^4.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "semver": "^7.3.7", + "ssri": "^13.0.0", + "treeverse": "^3.0.0", + "walk-up-path": "^4.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "10.8.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "ci-info": "^4.0.0", + "ini": "^6.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "7.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "5.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "glob": "^13.0.0", + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "9.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^20.0.0", + "json-parse-even-better-errors": "^5.0.0", + "pacote": "^21.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "7.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "10.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "4.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/core": { + "version": "3.2.0", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.5.0", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "4.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "4.0.2", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "3.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "4.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/npm/node_modules/bin-links": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "proc-log": "^6.0.0", + "read-cmd-shim": "^6.0.0", + "write-file-atomic": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "5.0.4", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "20.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.6.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "3.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.4.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "5.0.3", + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "2.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.4.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/diff": { + "version": "8.0.3", + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.3", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "13.0.6", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "9.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.2.0", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.7.2", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "8.2.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^7.0.0", + "npm-package-arg": "^13.0.0", + "promzard": "^3.0.1", + "read": "^5.0.1", + "semver": "^7.7.2", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "10.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "6.0.3", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^5.0.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/npm/node_modules/isexe": { + "version": "4.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "10.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^13.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "8.1.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.4.2", + "@npmcli/installed-package-contents": "^4.0.0", + "binary-extensions": "^3.0.0", + "diff": "^8.0.2", + "minimatch": "^10.0.3", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2", + "tar": "^7.5.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "10.2.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/arborist": "^9.4.2", + "@npmcli/package-json": "^7.0.0", + "@npmcli/run-script": "^10.0.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2", + "proc-log": "^6.0.0", + "read": "^5.0.1", + "semver": "^7.3.7", + "signal-exit": "^4.1.0", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "7.0.19", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.4.2" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "8.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "9.1.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.4.2", + "@npmcli/run-script": "^10.0.0", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "11.1.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^7.0.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^13.0.0", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.7", + "sigstore": "^4.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "9.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "8.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/run-script": "^10.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "11.2.7", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "15.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "10.2.4", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.1.3", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "5.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/negotiator": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "12.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "8.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "13.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "10.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "11.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "12.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "19.1.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "4.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "7.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/pacote": { + "version": "21.5.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "5.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^5.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "2.0.2", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "6.1.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/proggy": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "5.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "^3.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "4.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.0", + "@sigstore/tuf": "^4.0.1", + "@sigstore/verify": "^3.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.7", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.23", + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "13.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "10.2.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "7.5.11", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tinyglobby": { + "version": "0.2.15", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "4.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "5.0.0", + "license": "ISC", + "dependencies": { + "unique-slug": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "6.0.0", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "7.0.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/which": { + "version": "6.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "7.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "5.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.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==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oracledb": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.10.0.tgz", + "integrity": "sha512-kGUumXmrEWbSpBuKJyb9Ip3rXcNgKK6grunI3/cLPzrRvboZ6ZoLi9JQ+z6M/RIG924tY8BLflihL4CKKQAYMA==", + "hasInstallScript": true, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/os": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/os/-/os-0.1.2.tgz", + "integrity": "sha512-ZoXJkvAnljwvc56MbvhtKVWmSkzV712k42Is2mA0+0KTSRakq5XXuXpjZjgAt9ctzl51ojhQWakQQpmOvXWfjQ==" + }, + "node_modules/p-limit": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.2.0.tgz", + "integrity": "sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==" + }, + "node_modules/precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", + "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/sequelize": { + "version": "6.37.8", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz", + "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/serve-favicon": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.1.tgz", + "integrity": "sha512-JndLBslCLA/ebr7rS3d+/EKkzTsTi1jI2T9l+vHfAaGJ7A7NhtDpSZ0lx81HCNWnnE0yHncG+SSnVf9IMxOwXQ==", + "dependencies": { + "etag": "~1.8.1", + "fresh": "~0.5.2", + "ms": "~2.1.3", + "parseurl": "~1.3.2", + "safe-buffer": "~5.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-favicon/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "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==", + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==" + }, + "node_modules/string-width-cjs/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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tedious": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-18.6.1.tgz", + "integrity": "sha512-9AvErXXQTd6l7TDd5EmM+nxbOGyhnmdbp/8c3pw+tjaiSXW9usME90ET/CRG1LN1Y9tPMtz/p83z4Q97B4DDpw==", + "dependencies": { + "@azure/core-auth": "^1.7.2", + "@azure/identity": "^4.2.1", + "@azure/keyvault-keys": "^4.4.0", + "@js-joda/core": "^5.6.1", + "@types/node": ">=18", + "bl": "^6.0.11", + "iconv-lite": "^0.6.3", + "js-md4": "^0.3.2", + "native-duplexpair": "^1.0.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dependencies": { + "inherits": "2.0.3" + } + }, + "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==" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vasync": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", + "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "verror": "1.10.0" + } + }, + "node_modules/vasync/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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==", + "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/wrap-ansi-cjs/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==" + }, + "node_modules/wrap-ansi-cjs/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==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e86c12e --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "radixos", + "version": "0.9", + "description": "Webbasiertes Betriebssystem für das Grünflächenamt Frankfurt am Main", + "main": "server.js", + "_moduleAliases": { + "@root": "./", + "@plugins": "./plugins", + "@public": "./public", + "@source": "./src", + "@services": "./src/services", + "@models": "./src/models", + "@routes": "./src/routes", + "@sockets": "./src/sockets" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "author": "Grünflächenamt - Manuel Sowada", + "license": "UNLICENSED", + "licensefile": "license_internal.txt", + "dependencies": { + "activedirectory2": "^2.2.0", + "bcryptjs": "^3.0.2", + "body-parser": "^2.2.0", + "child_process": "^1.0.2", + "chokidar": "^4.0.3", + "cookie-parser": "^1.4.7", + "express": "^5.1.0", + "express-handlebars": "^8.0.3", + "fs": "^0.0.1-security", + "fs-extra": "^11.3.2", + "https": "^1.0.0", + "jsonwebtoken": "^9.0.2", + "ldapjs": "^3.0.7", + "module-alias": "^2.2.3", + "multer": "^2.0.2", + "net": "^1.0.2", + "npm": "^11.6.4", + "oracledb": "^6.10.0", + "os": "^0.1.2", + "p-limit": "^7.2.0", + "path": "^0.12.7", + "sequelize": "^6.37.7", + "serve-favicon": "^2.5.1", + "socket.io": "^4.8.1", + "tedious": "^18.6.1" + } +} diff --git a/public/helpers/comparisonHelpers.js b/public/helpers/comparisonHelpers.js new file mode 100644 index 0000000..c4416bb --- /dev/null +++ b/public/helpers/comparisonHelpers.js @@ -0,0 +1,72 @@ +const Handlebars = require('handlebars'); + +module.exports = { + toJSON: function(object) { + if(typeof object === 'object') { + return JSON.stringify(object); + } else { + throw 'no object type'; + } + }, + isObject: function(value, options) { + return typeof value === 'object' && value !== null && !Array.isArray(value) ? options.fn(this) : options.inverse(this); + }, + isArray: function(value, options) { + return Array.isArray(value) ? options.fn(this) : options.inverse(this); + }, + isRGB: function(value) { + return Array.isArray(value) && (value.length === 3 || value.length === 4); + }, + rgbString: function(value) { + if (Array.isArray(value)) { + return value.length === 3 + ? `rgb(${value.join(',')})` + : value.length === 4 + ? `rgba(${value.join(',')})` + : ''; + } + return ''; + }, + toArray: function(value) { + if (Array.isArray(value)) return value; + if (typeof value === "string") return [value]; + return []; + }, + replaceAll: function(string, pattern, replacement) { + return new Handlebars.SafeString(string.replaceAll(pattern, replacement) || ''); + }, + equaler: function(v1, operator, v2, options) { + switch (operator) { + case '==': + return (v1 == v2) ? options.fn(this) : options.inverse(this); + case '===': + return (v1 === v2) ? options.fn(this) : options.inverse(this); + case '!=': + return (v1 != v2) ? options.fn(this) : options.inverse(this); + case '!==': + return (v1 !== v2) ? options.fn(this) : options.inverse(this); + case '<': + return (v1 < v2) ? options.fn(this) : options.inverse(this); + case '<=': + return (v1 <= v2) ? options.fn(this) : options.inverse(this); + case '>': + return (v1 > v2) ? options.fn(this) : options.inverse(this); + case '>=': + return (v1 >= v2) ? options.fn(this) : options.inverse(this); + case '&&': + return (v1 && v2) ? options.fn(this) : options.inverse(this); + case '||': + return (v1 || v2) ? options.fn(this) : options.inverse(this); + case 'typeof': + return (typeof v1 === v2) ? options.fn(this) : options.inverse(this); + case 'like': + return (v1.indexOf(v2)) > -1 ? options.fn(this) : options.inverse(this); + case 'includes': + return (v1.includes(v2)) <= 0 ? options.inverse(this) : options.fn(this); + case '%': + return (v1 % v2) == 0 ? options.fn(this) : options.inverse(this); + default: + return options.inverse(this); + } + }, +}; \ No newline at end of file diff --git a/public/helpers/dateHelpers.js b/public/helpers/dateHelpers.js new file mode 100644 index 0000000..3993c4c --- /dev/null +++ b/public/helpers/dateHelpers.js @@ -0,0 +1,6 @@ +// const moment = require('moment'); // npm install moment +// moment.locale('de'); + +module.exports = { + dateFormat: (date, format) => dateFormat(date, format) +}; \ No newline at end of file diff --git a/public/helpers/fileHelper.js b/public/helpers/fileHelper.js new file mode 100644 index 0000000..aafcd54 --- /dev/null +++ b/public/helpers/fileHelper.js @@ -0,0 +1,24 @@ +module.exports = { + // objectTree: function(context, options) { + // let ret = ''; + + // if (context === null || context === undefined) return ret; + + // if (Array.isArray(context)) { + // context.forEach((item, index) => { + // ret += options.fn({ key: index, value: item }); + // }); + // } else if (typeof context === 'object') { + // for (const key in context) { + // if (context.hasOwnProperty(key)) { + // ret += options.fn({ key, value: context[key] }); + // } + // } + // } else { + // // primitive Werte + // ret += options.fn({ key: null, value: context }); + // } + + // return ret; + // } +} \ No newline at end of file diff --git a/public/helpers/iterationHelpers.js b/public/helpers/iterationHelpers.js new file mode 100644 index 0000000..8ac3a5f --- /dev/null +++ b/public/helpers/iterationHelpers.js @@ -0,0 +1,145 @@ +module.exports = { + objectTree: function(obj) { + function traverse(o) { + if (o === null || typeof o !== 'object') return o; + + if (Array.isArray(o)) { + // RGB oder RGBA-Erkennung: Array aus 3 oder 4 Zahlen + if ((o.length === 3 || o.length === 4) && o.every(v => typeof v === 'number')) { + // const rgbString = o.length === 3 + // ? `rgb(${o.join(',')})` + // : `rgba(${o.join(',')})`; + const rgbString = o.join(','); + return { key: null, value: rgbString, children: null, isRGB: true }; + } + + // normales Array + return o.map((v, i) => ({ + key: i, + value: typeof v !== 'object' ? v : null, + children: typeof v === 'object' ? traverse(v) : null + })); + } + + const result = []; + for (const key in o) { + if (o.hasOwnProperty(key)) { + const val = o[key]; + const children = (val !== null && typeof val === 'object') ? traverse(val) : null; + result.push({ + key, + value: children ? null : val, + children + }); + } + } + return result; + } + return traverse(obj); + }, + jsonEntriesRecursive: function(obj, options) { + const entries = []; + + function traverse(prefix, data) { + if (Array.isArray(data)) { + data.forEach((item, index) => { + const arrayKey = `${prefix}[${index}]`; + if (typeof item === "object" && item !== null) { + traverse(arrayKey, item); // Rekursion für Array-Objekte + } else { + entries.push({ key: arrayKey, value: item }); + } + }); + } else if (typeof data === "object" && data !== null) { + for (const key in data) { + if (!Object.prototype.hasOwnProperty.call(data, key)) continue; + + const value = data[key]; + const fullKey = prefix ? `${prefix}.${key}` : key; + + if (typeof value === "object" && value !== null) { + traverse(fullKey, value); // Rekursion für verschachtelte Objekte + } else { + entries.push({ key: fullKey, value }); + } + } + } else { + // Primitive Werte am obersten Level + entries.push({ key: prefix, value: data }); + } + + console.log({ key: prefix, value: data }) + } + + traverse("", obj); + + // Handlebars-Block Helper + if (options.fn) { + return options.fn(entries); + } + + return entries; + }, + groupBy: function(items, keys, options) { + // keys kann String oder Array sein + if (!Array.isArray(keys)) keys = [keys]; + if (!keys.length) return ''; + + const key = keys[0]; // aktueller Gruppierungsschlüssel + const remainingKeys = keys.slice(1); // restliche Schlüssel + + // Gruppieren nach aktuellem Schlüssel + const groups = {}; + items.forEach(item => { + let groupKeys = item[key]; + if (!Array.isArray(groupKeys)) groupKeys = [groupKeys]; + + groupKeys.forEach(groupKey => { + if (!groups[groupKey]) groups[groupKey] = []; + groups[groupKey].push(item); + }); + }); + + // Ergebnis zusammensetzen + let result = ''; + for (const group in groups) { + const groupItems = groups[group]; + + if (remainingKeys.length > 0) { + // Rekursive Verschachtelung + // Wir rufen die gleiche Helper-Funktion intern auf + const nestedResult = Handlebars.helpers.groupBy(groupItems, remainingKeys, options); + result += options.fn({ key: group, items: nestedResult }); + } else { + // Basisfall: keine weiteren Keys, items direkt weitergeben + result += options.fn({ key: group, items: groupItems }); + } + } + + return result; + }, + parseArray: function(str){ + try { + return JSON.parse(str); + } catch (e) { + return []; + } + }, + ifSingle: function(array, options) { + if (Array.isArray(array) && array.length === 1) { + return options.fn(array[0]); // Item direkt übergeben + } + return options.inverse(this); // else-Block + }, + findValue: function(array, searchKey, searchValue, returnKey) { + if (!Array.isArray(array)) return ""; + for (let i = 0; i < array.length; i++) { + const item = array[i]; + + if (item && item[searchKey] === searchValue) { + return item[returnKey] ?? ""; + } + } + return ""; + } +}; \ No newline at end of file diff --git a/public/images/app.png b/public/images/app.png new file mode 100644 index 0000000000000000000000000000000000000000..77da91af2275808969620b187532ecb9e72aaae3 GIT binary patch literal 729 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 literal 0 HcmV?d00001 diff --git a/public/images/arrow_right.png b/public/images/arrow_right.png new file mode 100644 index 0000000000000000000000000000000000000000..ba7a3daab3d1220f0974f2408937de71a6fc1f0b GIT binary patch literal 2890 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 literal 0 HcmV?d00001 diff --git a/public/images/cursor_dark.png b/public/images/cursor_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a35a38170ecc38a53c55e09af3ff62960cc85921 GIT binary patch literal 957 zcmV;u148_XP)EX>4Tx04R}tk-tmBKpe$i(-wbFaj=7kLx$>P7cC;V)G8FALZ}s5buhW~7c^-| zQd}Gb*MfsT7OM^}&bm6d3WDGd5dQ#iQgo3Lzn2zT#CUMrhj;fLckck9QDK@Dhy$8# zo9Sdi%;r|bo>v6WhahGVlbL1ANm2@)kYC7c$ z*%qsuw>WE+8f)K^zc7^7SC+X>a}-G|VhIvND5#-~3T#Aa)k(3Cru~G6f5i1mcbxBI$7!Ab{%7Dy@A#_?VCIwb zdPj>K0fXDX#dSwh_JGSBVED<9P1%+FG=)L|ct4|W$^k>SK>u2+xAs0xAAk&XmAU~A z4uSC^Wv_d@ySKZyf6ui0`vK&Za@1%PNmT#<00v@9M??T)0A>L3p)tey00009a7bBm z000XU000XU0RWnu7ytkO2XskIMF;8x2?i||Bk6%CC;&JQt_FIF2J`MiN@<8`pJT9}P$; zzcVu?lL>s^Pa&n$ul0KUHk-}fzY_?<@SK^kTCE_2K(pDzY&J`w?RM*jVR+qYwI2T$ zA)=qmj7FmYAq0dFsMTt490%4~Ff)uXTPfv-e!u_Z05Hb9VrF=rmjX$YN+k@3Ll|R{ z9ziMf#dY0hDNwK1?-9{0GoxCq9w0&pjMKDE{&iXwD69WXO?yWRCk_o21^mdyOmT3?(5gkkt%zvG_gJv#YHrIa7F f)=WfKcm4Yf$i}&apjjW)00000NkvXXu0mjfF^;Wy literal 0 HcmV?d00001 diff --git a/public/images/cursor_light.png b/public/images/cursor_light.png new file mode 100644 index 0000000000000000000000000000000000000000..0cc811e7573eff6eb4fb21d4fc84b806c7fbc1fd GIT binary patch literal 5590 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 literal 0 HcmV?d00001 diff --git a/public/images/cursor_pointer_light.png b/public/images/cursor_pointer_light.png new file mode 100644 index 0000000000000000000000000000000000000000..1ae620114bc68c955fe3aac344db02c327f52869 GIT binary patch literal 5781 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 HShnmp;U)JQ literal 0 HcmV?d00001 diff --git a/public/images/eventlog.ico b/public/images/eventlog.ico new file mode 100644 index 0000000000000000000000000000000000000000..9f4e30d26b2a9f26a347f3b761043483e7207b5f GIT binary patch literal 19518 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 Gj{JW>o}O?3 literal 0 HcmV?d00001 diff --git a/public/images/help.png b/public/images/help.png new file mode 100644 index 0000000000000000000000000000000000000000..b3f723ba269a1c6f34c8bfccae05fcc6abadef0f GIT binary patch literal 3310 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 literal 0 HcmV?d00001 diff --git a/public/images/notifybubble.png b/public/images/notifybubble.png new file mode 100644 index 0000000000000000000000000000000000000000..5502015604fba4c235b2780d8fa6c58a382e13aa GIT binary patch literal 3469 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 literal 0 HcmV?d00001 diff --git a/public/images/serverinfo.png b/public/images/serverinfo.png new file mode 100644 index 0000000000000000000000000000000000000000..9173e38999f5559053b6479769f89231c4030108 GIT binary patch literal 914 zcmV;D18w|?P)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B00(qQO+^Rl1QP`$DS5Vc)Bpei z=t)FDRCwC$omox;K@dYztsDmKe;9Hb_(vd^q!+vV-Zv6mw!3NZ~K0L&C~w~)?bmk7WN0aa#Chgz&=kLpg^xvp{$ z$>nCvvBpA}>{?4z5cj8SE_D|oWY=GqpB;iyDZ2anc&eJ*6~x(5=wAk`9>d4uQHswz z>U7c-%vT)Ct}zwnXOC&H?#?^MJ2E=YG*zzFzA}^p^Rq)R)C(sB;!8lh%bu#nRSf36+$3NIic=PBxwR#XrqW(5!hcT`i^ZgbC z8_`Ki0B8R`%fMj9Wp^o#f#F!^5dvI=AZpyaNy%fLDx64Z*=3XCs8TnbV&BjnX~G5o$8eKdw>(dbz0VHrx>%_=V>Em)pJtA z!OY3)U{eFYoq%7*Il2d+3utLkr;0hTek#pIm49T_;;Ui1L(s+i7RSKd`;cyXo`?@e zI zM_1))PNwAm5j2U1u o000000000000000006}C1+_*C2JNnXHUIzs07*qoM6N<$g4&0K)&Kwi literal 0 HcmV?d00001 diff --git a/public/images/tutorial.png b/public/images/tutorial.png new file mode 100644 index 0000000000000000000000000000000000000000..d4351a4f244274a7c00183199ee863521129b130 GIT binary patch literal 4074 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 literal 0 HcmV?d00001 diff --git a/public/javascript/JSON.js b/public/javascript/JSON.js new file mode 100644 index 0000000..f78b59f --- /dev/null +++ b/public/javascript/JSON.js @@ -0,0 +1,361 @@ +/* +createJsonTree({ + container: domElement, + data: jsonData, + onChange: (data) => { }, // optional: callback on change + onSave: (data) => { } // optional: if set, a save button will be added +}); +*/function createJsonTree({ + container, + data, + onChange = () => {}, + onSave = null +}) { + container.innerHTML = ""; + container.classList.add("json-tree-root"); + + const history = []; + const redoStack = []; + + let lastSnapshot = clone(data); + + function clone(obj) { + return JSON.parse(JSON.stringify(obj)); + } + + function diff(oldObj, newObj) { + const changes = {}; + + function walk(o, n, path = "") { + const keys = new Set([ + ...Object.keys(o || {}), + ...Object.keys(n || {}) + ]); + + for (const key of keys) { + const oldVal = o?.[key]; + const newVal = n?.[key]; + + const currentPath = path ? `${path}.${key}` : key; + + if ( + typeof oldVal === "object" && + oldVal !== null && + typeof newVal === "object" && + newVal !== null && + !Array.isArray(oldVal) + ) { + walk(oldVal, newVal, currentPath); + continue; + } + + if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) { + changes[currentPath] = newVal; + } + } + } + + walk(oldObj, newObj); + return changes; + } + + function pushHistory() { + history.push(clone(data)); + redoStack.length = 0; + } + + function undo() { + if (!history.length) return; + redoStack.push(clone(data)); + data = history.pop(); + render(); + } + + function redo() { + if (!redoStack.length) return; + history.push(clone(data)); + data = redoStack.pop(); + render(); + } + + function save() { + if (onSave) onSave(data); + } + + function autoResize(input) { + input.style.width = "10px"; + input.style.width = input.scrollWidth + "px"; + } + + function remove(key, parent) { + if (key === null || parent == null) return; + + if (!confirm("Wirklich löschen?")) return; + + pushHistory(); + + if (Array.isArray(parent)) { + parent.splice(key, 1); + } else { + delete parent[key]; + } + + onChange(data); + render(); + } + + function render() { + container.innerHTML = ""; + + const controls = document.createElement("div"); + controls.className = "json-controls"; + + const undoBtn = document.createElement("button"); + undoBtn.className = "monolyth"; + undoBtn.textContent = "Undo"; + undoBtn.onclick = undo; + + const redoBtn = document.createElement("button"); + redoBtn.className = "monolyth"; + redoBtn.textContent = "Redo"; + redoBtn.onclick = redo; + + controls.appendChild(undoBtn); + controls.appendChild(redoBtn); + + if (onSave) { + const saveBtn = document.createElement("button"); + saveBtn.className = "monolyth"; + saveBtn.textContent = "Save"; + saveBtn.onclick = save; + controls.appendChild(saveBtn); + } + + container.appendChild(controls); + container.appendChild(renderNode(data, null, null, "root", 0)); + } + + function renderNode(value, key, parent, path, level) { + const wrapper = document.createElement("div"); + wrapper.className = "json-line"; + wrapper.style.marginLeft = `${level * 18}px`; + + const keySpan = document.createElement("span"); + keySpan.className = "json-key"; + + if (key !== null) { + keySpan.textContent = `"${key}": `; + } + + const removeBtn = document.createElement("span"); + removeBtn.className = "json-remove"; + removeBtn.textContent = " [x]"; + removeBtn.onclick = (evt) => { + evt.stopPropagation(); + remove(key, parent); + }; + + // OBJECT / ARRAY + if (typeof value === "object" && value !== null) { + const isArray = Array.isArray(value); + + const header = document.createElement("div"); + header.className = "json-header"; + + const toggle = document.createElement("span"); + toggle.className = "json-toggle"; + toggle.textContent = isArray ? "[ ]" : "{ }"; + + const keyLabel = document.createElement("span"); + keyLabel.className = "json-key"; + if (key !== null) keyLabel.textContent = `"${key}": `; + + const addBtn = document.createElement("span"); + addBtn.className = "json-add"; + addBtn.textContent = " [+]"; + + const children = document.createElement("div"); + children.className = "json-children"; + + keyLabel.onclick = toggle.onclick = () => { + children.classList.toggle("collapsed"); + }; + + addBtn.onclick = () => { + let newKey = ""; + let newValue; + + feedbox({ + title: `Key hinzufügen`, + message: ` +
+ ${!isArray ? ` + + ` : ``} + +
+ `, + buttons: { + yes: { + text: 'Anlegen', + onClick: () => { + pushHistory(); + + if (!isArray) { + const input = document.querySelector('#newJsonKeyName'); + newKey = input?.value?.trim(); + + if (!newKey) return; + } + + const selectedType = document.querySelector('#newJsonValueType').value; + + switch (selectedType) { + case "number": newValue = 0; break; + case "boolean": newValue = false; break; + case "object": newValue = {}; break; + case "array": newValue = []; break; + default: newValue = ""; + } + + if (isArray) { + value.push(newValue); + } else { + value[newKey] = newValue; + } + + onChange(data); + render(); + } + }, + no: { text: 'Abbrechen' } + } + }); + }; + + const inner = document.createElement("div"); + + const entries = isArray + ? value.map((v, i) => [i, v]) + : Object.entries(value); + + entries.forEach(([k, v]) => { + inner.appendChild( + renderNode(v, k, value, `${path}.${k}`, level + 1) + ); + }); + + children.appendChild(inner); + + header.appendChild(keyLabel); + header.appendChild(toggle); + header.appendChild(addBtn); + + if (key !== null) header.appendChild(removeBtn); + + wrapper.appendChild(header); + wrapper.appendChild(children); + + return wrapper; + } + + // STRING + if (typeof value === "string") { + const input = document.createElement("input"); + input.className = "json-input string"; + input.value = value; + + input.oninput = () => { + pushHistory(); + parent[key] = input.value; + autoResize(input); + onChange(data); + }; + + setTimeout(() => autoResize(input), 0); + + wrapper.appendChild(keySpan); + wrapper.appendChild(input); + if (key !== null) wrapper.appendChild(removeBtn); + return wrapper; + } + + // NUMBER + if (typeof value === "number") { + const input = document.createElement("input"); + input.type = "number"; + input.className = "json-input number"; + input.value = value; + + input.oninput = () => { + pushHistory(); + parent[key] = Number(input.value); + autoResize(input); + onChange(data); + }; + + setTimeout(() => autoResize(input), 0); + + wrapper.appendChild(keySpan); + wrapper.appendChild(input); + if (key !== null) wrapper.appendChild(removeBtn); + return wrapper; + } + + // BOOLEAN + if (typeof value === "boolean") { + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = value; + + input.onchange = () => { + pushHistory(); + parent[key] = input.checked; + onChange(data); + }; + + wrapper.appendChild(keySpan); + wrapper.appendChild(input); + if (key !== null) wrapper.appendChild(removeBtn); + return wrapper; + } + + const span = document.createElement("span"); + span.textContent = String(value); + + wrapper.appendChild(keySpan); + wrapper.appendChild(span); + if (key !== null) wrapper.appendChild(removeBtn); + + return wrapper; + } + + pushHistory(); + render(); + + return { + undo, + redo, + save, + refresh: render, + getData: () => data, + getChanges() { + return diff(lastSnapshot, data); + }, + commit() { + lastSnapshot = clone(data); + }, + update(newData) { + data = newData; + lastSnapshot = clone(newData); + pushHistory(); + render(); + } + }; +} \ No newline at end of file diff --git a/public/javascript/contextMenu.js b/public/javascript/contextMenu.js new file mode 100644 index 0000000..87493be --- /dev/null +++ b/public/javascript/contextMenu.js @@ -0,0 +1,244 @@ +/* +const ctx = new ContextMenu(); + +ctx.setItems([ + { label: "Öffnen", disable: true, onClick: () => alert("Öffnen!") }, + { label: "Öffnen", onClick: () => alert("Öffnen!") }, + { label: "Bearbeiten", onClick: () => alert("Bearbeiten…") }, + { type: "divider" }, + { + label: "Mehr Optionen", + color: 'rgb()|color-name|color-value', + children: [ + { label: "Duplizieren", onClick: () => alert("Dupliziert!") }, + { label: "Umbenennen", onClick: () => alert("Rename…") }, + { + label: "Exportieren", + children: [ + { label: "PDF", onClick: () => alert("PDF export") }, + { label: "CSV", onClick: () => alert("CSV export") }, + ] + } + ] + }, + { label: "Löschen", onClick: () => alert("Gelöscht!") }, +]); + +ctx.show(element, or location, { position: "right" }); +*/ + + +class ContextMenu { + constructor() { + this.menu = document.createElement("div"); + this.menu.className = "ctx-menu hidden"; + document.body.appendChild(this.menu); + + // close on click outside → sofort + document.addEventListener("click", (e) => { + if (!this.menu.contains(e.target)) { + this.closeAllSubmenus(); + this.hide(); + } + }); + + window.addEventListener("resize", () => this.hide()); + window.addEventListener("scroll", () => this.hide()); + } + + setItems(items) { + this.menu.innerHTML = ""; + this.menu.appendChild(this.buildMenu(items)); + } + + buildMenu(items) { + const ul = document.createElement("ul"); + ul.className = "ctx-list"; + + items.forEach(item => { + if (item.type === "divider") { + const divider = document.createElement("li"); + divider.className = "ctx-divider"; + ul.appendChild(divider); + return; + } + + const li = document.createElement("li"); + li.className = "ctx-item"; + + if (item.color) li.style.boxShadow = `inset 5px 0px 0px 0px ${item.color}`; + if (item.disabled) li.classList.add("ctx-disabled"); + + li.innerHTML = ` + ${item.label} + ${item.children ? "" : ""} + `; + + if (item.onClick && !item.children && !item.disabled) { + li.addEventListener("click", e => { + e.stopPropagation(); + item.onClick(e); + this.hide(); // Hauptmenü sofort schließen + }); + } + + if (item.children) { + const submenu = this.buildMenu(item.children); + submenu.classList.add("ctx-submenu"); + li.appendChild(submenu); + + let hideTimeout; + + const openSubmenu = () => { + clearTimeout(hideTimeout); + submenu.classList.add("open"); + + submenu.style.left = ""; + submenu.style.top = ""; + + const liRect = li.getBoundingClientRect(); + const submenuRect = submenu.getBoundingClientRect(); + + let left = li.offsetWidth; + if (liRect.right + submenuRect.width > window.innerWidth) left = -submenuRect.width; + submenu.style.left = left + "px"; + + let top = 0; + if (liRect.top + submenuRect.height > window.innerHeight) { + top = window.innerHeight - (liRect.top + submenuRect.height) - 4; + } + submenu.style.top = top + "px"; + }; + + const closeSubmenu = (delay = 500) => { + clearTimeout(hideTimeout); + hideTimeout = setTimeout(() => { + submenu.classList.remove("open"); + submenu.style.left = ""; + submenu.style.top = ""; + }, delay); + }; + + li.addEventListener("mouseenter", openSubmenu); + li.addEventListener("mouseleave", () => closeSubmenu(500)); + submenu.addEventListener("mouseenter", () => clearTimeout(hideTimeout)); + submenu.addEventListener("mouseleave", () => closeSubmenu(500)); + } + + ul.appendChild(li); + }); + + return ul; + } + + closeAllSubmenus() { + this.menu.querySelectorAll(".ctx-submenu.open").forEach(sm => { + sm.classList.remove("open"); + sm.style.left = ""; + sm.style.top = ""; + }); + } + + show(target, options = {}) { + this.closeAllSubmenus(); + + let x, y; + + const position = options.position || "right"; // default + const offset = options.offset || 4; + + // 🖱️ Mausposition + if (typeof target === "number") { + x = target; + y = options.y; + } + + // 📦 Element als Anchor + else if (target instanceof HTMLElement) { + const rect = target.getBoundingClientRect(); + + switch (position) { + case "right": + x = rect.right + offset; + y = rect.top; + break; + + case "left": + x = rect.left - this.menu.offsetWidth - offset; + y = rect.top; + break; + + case "top": + x = rect.left; + y = rect.top - this.menu.offsetHeight - offset; + break; + + case "bottom": + default: + x = rect.left; + y = rect.bottom + offset; + break; + } + } + + // ❌ invalid fallback + else { + return; + } + + // 👉 erstmal anzeigen (wichtig für width/height!) + this.menu.style.left = "0px"; + this.menu.style.top = "0px"; + this.menu.classList.add("show"); + this.menu.classList.remove("hidden"); + + const rect = this.menu.getBoundingClientRect(); + + // 🧠 Reposition nach echten Maßen + if (target instanceof HTMLElement) { + const anchor = target.getBoundingClientRect(); + + switch (position) { + case "right": + x = anchor.right + offset; + y = anchor.top; + break; + + case "left": + x = anchor.left - rect.width - offset; + y = anchor.top; + break; + + case "top": + x = anchor.left; + y = anchor.top - rect.height - offset; + break; + + case "bottom": + x = anchor.left; + y = anchor.bottom + offset; + break; + } + } + + // 🧱 Screen Bounds + if (x + rect.width > window.innerWidth) { + x = window.innerWidth - rect.width - 4; + } + + if (y + rect.height > window.innerHeight) { + y = window.innerHeight - rect.height - 4; + } + + if (x < 0) x = 4; + if (y < 0) y = 4; + + this.menu.style.left = x + "px"; + this.menu.style.top = y + "px"; + } + + hide() { + this.menu.classList.remove("show"); + setTimeout(() => this.menu.classList.add("hidden"), 200); + } +} diff --git a/public/javascript/customModal.js b/public/javascript/customModal.js new file mode 100644 index 0000000..9797456 --- /dev/null +++ b/public/javascript/customModal.js @@ -0,0 +1,259 @@ +let activeFeedbox = null; + + +//#region Messagebox +function getMessageColorByLevelId(levelId) { + return levelId == -1 ? 'test' : + levelId == 0 ? 'success' : + levelId == 1 ? 'info' : + levelId == 2 ? 'warn' : + levelId == 4 ? 'error' : 'throw_exception'; +} + +/** + * Shows messages in a containerbox on the right + * @param {string} title - Title + * @param {string} text - Message content + * @param {string} levelId - 1=info | 2=warn | 4=error | 8=throw_exception | 16=success + * @param {string} [targetId] - Which element is getting focused + * @param {number} duration - Time in ms until auto-close + */ +// function showMessage(title, text, levelId = 1, targetId = null, duration = 4000) { +function showMessage(title, text, levelId = 1, onclick = null, duration = 4000) { + // Falls kein Container existiert → automatisch anlegen + let container = document.getElementById('message-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'message-container'; + container.style.position = 'fixed'; + container.style.top = '20px'; + container.style.right = '20px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.alignItems = 'flex-end'; + container.style.zIndex = '9999'; + document.body.appendChild(container); + } + const message = document.createElement('div'); + message.classList.add('message', getMessageColorByLevelId(levelId)); + + // --- HEADER --- + const header = document.createElement('div'); + header.className = 'message-header'; + + const titleContainer = document.createElement('div'); + titleContainer.className = 'message-title'; + titleContainer.innerHTML = title; + + const pinDiv = document.createElement('div'); + pinDiv.className = 'pin-div'; + pinDiv.innerHTML = '📌'; + pinDiv.title = 'Anpinnen'; + + const countdown = document.createElement('span'); + countdown.className = 'countdown'; + countdown.textContent = `${(duration / 1000).toFixed(1)}s`; + + // Titel + Countdown + Pin in eine Zeile + header.appendChild(titleContainer); + header.appendChild(countdown); + header.appendChild(pinDiv); + + // --- BODY --- + const body = document.createElement('div'); + body.className = 'message-text'; + body.innerHTML = text; + + message.appendChild(header); + message.appendChild(body); + container.appendChild(message); + + // --- LOGIK --- + let pinned = false; + let remainingTime = duration; + let interval = null; + let lastTick = Date.now(); + + function startCountdown() { + lastTick = Date.now(); + clearInterval(interval); + interval = setInterval(() => { + const now = Date.now(); + const delta = now - lastTick; + lastTick = now; + remainingTime -= delta; + + if (remainingTime <= 0 && !pinned) { + clearInterval(interval); + slideOutMessage(message); + } else if (!pinned) { + countdown.textContent = `${(remainingTime / 1000).toFixed(1)}s`; + } + }, 100); + } + + function stopCountdown() { + clearInterval(interval); + } + + // Start Countdown + startCountdown(); + + // Hover pausiert Countdown + message.addEventListener('mouseenter', stopCountdown); + message.addEventListener('mouseleave', () => { + if (!pinned) startCountdown(); + }); + + // Klick auf Nachricht → schließen (nur wenn nicht gepinnt) + message.addEventListener('click', (e) => { + if(onclick) { + onclick(); + } + // if (e.target === pinDiv) return; + // if (targetId) { + // const target = document.getElementById(targetId); + // if (target) target.scrollIntoView({ behavior: 'smooth' }); + // } + if (!pinned) slideOutMessage(message); + }); + + // Klick auf Pin → anheften / lösen + pinDiv.addEventListener('click', (e) => { + e.stopPropagation(); + pinned = !pinned; + if (pinned) { + pinDiv.classList.add('pinned'); + pinDiv.title = 'Loslösen'; + stopCountdown(); + countdown.textContent = '📍 Angeheftet'; + } else { + pinDiv.classList.remove('pinned'); + pinDiv.title = 'Anpinnen'; + startCountdown(); + } + }); +} + +function slideOutMessage(message) { + if (!message) return; + message.style.animation = 'slideOut 0.5s forwards'; + setTimeout(() => { + if (message.parentNode) message.parentNode.removeChild(message); + }, 300); +} + + +//#region Feedbox + +/** + * feedbox({ + * title: `⚠ Upload abbrechen?`, + * message: ` + *

Es laufen noch aktive Uploads.

+ *

Möchtest du wirklich alle abbrechen?

+ * `, + * buttons: { + * yes: { + * text: 'Ja, abbrechen', + * onClick: () => stopUploadQueue() + * }, + * no: { + * text: 'Weiter hochladen' + * }, + * cancel: { + * text: 'Zurück' + * } + * } + *}); +*/ +function feedbox({ + title = '', + message = '', + buttons = {}, // buttons: { yes: { text: 'Yes', onClick: () => { } } } + primary = null, // name of the primary button, to accept with enter-key + lock = false, // locks desktop + replace = false // replaces an actually shown feedbox +}) { + // 🚫 Nested verhindern + if (activeFeedbox) { + if (!replace) { + return Promise.resolve('blocked'); + } + activeFeedbox.close('replaced'); + } + + return new Promise(resolve => { + + const overlay = document.createElement('div'); + overlay.className = 'feedbox-overlay'; + + const box = document.createElement('div'); + box.className = 'feedbox'; + + const h = document.createElement('h3'); + h.innerHTML = title; + + const msg = document.createElement('div'); + msg.className = 'feedbox-message'; + msg.innerHTML = message; + + const actions = document.createElement('div'); + actions.className = 'feedbox-actions'; + + const btnMap = {}; + + Object.entries(buttons).forEach(([key, cfg]) => { + if (!cfg) return; + + const btn = document.createElement('button'); + btn.className = `feedbox-btn ${key}`; + btn.innerHTML = cfg.text ?? key; + + btn.onclick = () => { + cfg.onClick?.(); + close(key); + }; + + btnMap[key] = btn; + actions.appendChild(btn); + }); + + function close(result) { + document.removeEventListener('keydown', keyHandler); + overlay.remove(); + activeFeedbox = null; + resolve(result); + } + + function keyHandler(e) { + if (e.key === 'Escape') close('cancel'); + if (e.key === 'Enter' && primary && btnMap[primary]) { + btnMap[primary].click(); + } + } + + if (!lock) { + overlay.onclick = e => { + if (e.target === overlay) close('cancel'); + }; + } + + document.addEventListener('keydown', keyHandler); + + box.append(h, msg, actions); + overlay.appendChild(box); + document.body.appendChild(overlay); + + // Fokus + if (primary && btnMap[primary]) { + setTimeout(() => btnMap[primary].focus(), 0); + } + + // 🔐 Singleton setzen + activeFeedbox = { close }; + }); +} + +//#endregion + \ No newline at end of file diff --git a/public/javascript/loadOnce.js b/public/javascript/loadOnce.js new file mode 100644 index 0000000..ed9c33d --- /dev/null +++ b/public/javascript/loadOnce.js @@ -0,0 +1,97 @@ +const auth = { objectGuid: getCookie('ObjectGUID'), sAMAccountName: getCookie('sAMAccountName') }; +const mainSocket = io.connect('/', { reconnect: true, auth: auth }); +const adminSocket = io.connect('/admin', { reconnect: true, auth: auth }); + +const container = document.getElementById('message-container'); + + +adminSocket.on('eventlog', (data) => { + showMessage('EventLog', + `${data.datetime}
${[-1,0].includes(data.levelId) ? '' : data.trace}

${(Array.isArray(data.message) ? data.message.split('\r\n').join('
').split('\t').join(' ') : data.message.split('\r\n').join('
').split('\t').join(' '))}`, + data.levelId, + () => { + + } , + 10000); +}); + +mainSocket.on('event', (data) => { + showMessage(data.pluginName, + `[${data.datetime}]

${(Array.isArray(data.message) ? data.message.split('\r\n').join('
').split('\t').join(' ') : data.message.split('\r\n').join('
').split('\t').join(' '))}`, + data.levelId, + () => { + + } , + 10000); +}); + + + +//-1=test, 0=success, 1=log, 2=warn, 4=error, 8=throw_exception +function writeEventLog(levelId, pluginName, message) { + adminSocket.emit('eventlog', { + objectGuid: getCookie('ObjectGUID'), + levelId: levelId, + pluginName: pluginName, + message: message.stack === undefined ? message : { message: message.message, stack: message.stack } + }); +} + +// levelId: if -1, then write no log entry +// sendToParams: where clause to find objectGUIDs to send +function sendUserEvent(pluginName, message, sendToParams, levelId = -1) { + mainSocket.emit('event', { + objectGuid: getCookie('ObjectGUID'), + levelId: levelId, + pluginName: pluginName, + message: message.stack === undefined ? message : { message: message.message }, + sendToParams: sendToParams + }); +} + + +/* +{ + "status":"unload", + "pluginName":"user_management", + "metadata":{ + "name":"user_management", + "menuName":"User Management", + "description":"Beschreibung hier einfügen", + "version":"0.9.25.11.14", + "icon":"", + "permissions":[], + "config":{ + }, + "active":false + }, + "levelId":0, + "message":"Plugin user_management entladen", + "authorized": true +} +*/ +mainSocket.on('plugin_status', payload => { + const startMenuItem = document.querySelector(`[data-appname=${payload.metadata.name}]`); + if(['load', 'unload', 'update'].includes(payload.status)) { + if(payload.status == 'load') { + if(payload.authorized) { + startMenuItem.classList.remove('unload'); + startMenuItem.setAttribute('data-active', true); + } else { + startMenuItem.classList.add('unload'); + startMenuItem.setAttribute('data-active', false); + } + } else if(payload.status == 'unload') { + startMenuItem.classList.add('unload'); + startMenuItem.setAttribute('data-active', false); + } else if(payload.status == 'update') { + if(payload.authorized && payload.metadata.active) { + startMenuItem.classList.remove('unload'); + startMenuItem.setAttribute('data-active', true); + } else if(!payload.authorized){ + startMenuItem.classList.add('unload'); + startMenuItem.setAttribute('data-active', false); + } + } + } +}); \ No newline at end of file diff --git a/public/javascript/main.js b/public/javascript/main.js new file mode 100644 index 0000000..8148f27 --- /dev/null +++ b/public/javascript/main.js @@ -0,0 +1,1494 @@ +const restartHooks = []; + +let tableFetchController = null +let root = document.querySelector(":root"); +let copyToastTimeout; + + +//#region Systemwide colors +const COLORS = { + BLUE: 'rgb(100, 149, 237)', + GRAY: 'rgb(128, 128, 128)', + GREEN: 'rgb(198, 244, 180)', + YELLOW: 'rgb(255, 230, 153)', + RED: 'rgb(248, 191, 173)', +} +//#endregion + + +//#region Restart +function addRestartHook(fn) { + if (typeof fn === "function") restartHooks.push(fn); +} + +function restart() { + restartHooks.forEach(fn => fn()); + location.reload(); +} + +//#endregion + + +//#region CSS + + // sets the css root variable name by value // + function setCSSVariable(name, value) { + root.style.setProperty(`--${name}`, value); + } + + // gets the value of asked css variable name // + function getCSSVariable(name) { + return getComputedStyle(root).getPropertyValue(`--${name}`).trim(); + } + + + +async function loadServerStyles(theme = 'dark') { + const res = await fetch('models/stylesheet.json'); + const json = await res.json(); + + function jsonToCssVars(obj, prefix = '') { + let vars = []; + for (const key in obj) { + const value = obj[key]; + const newPrefix = prefix ? `${prefix}-${key}` : key; + + if (Array.isArray(value)) { + if (value.length === 3) vars.push(`--theme-${newPrefix}: rgb(${value.join(', ')});`); + else if (value.length === 4) vars.push(`--theme-${newPrefix}: rgba(${value.join(', ')});`); + } else if (typeof value === 'object' && value !== null) { + vars.push(jsonToCssVars(value, newPrefix)); + } else if (value !== '') { + vars.push(`--theme-${newPrefix}: ${value};`); + } + // console.log(vars.join('\n')) + } + return vars.join('\n'); + } + + const themeVars = json.theme[theme]; + const css = `:root {\n${jsonToCssVars(themeVars)}\n}`; + + let styleTag = document.getElementById('theme-styles'); + if (!styleTag) { + styleTag = document.createElement('style'); + styleTag.id = 'theme-styles'; + document.head.appendChild(styleTag); + } + styleTag.textContent = css; + document.getElementById('start-menu-icon').src = `../images/radix_os_${theme}_img.png`; + + return true; +} + +function switchTheme(themeName) { + setCookie('theme', themeName); // Theme direkt im Cookie speichern + savedTheme = themeName; + loadServerStyles(themeName); +} + +function setFontFamily(fontFamily) { + setCookie('fontfamily', fontFamily); // FontFamily direkt im Cookie speichern + setCSSVariable('fontFamily', fontFamily) + document.documentElement.style.fontFamily = fontFamily +} + +function setFontSize(fontSize) { + setCookie('fontsize', fontSize); // FontSize direkt im Cookie speichern + setCSSVariable('fontSize', fontSize + 'px') + document.documentElement.style.fontSize = fontSize + 'px' +} + +//#endregion + + + + + +function copyToClipboard(text) { + navigator.clipboard.writeText(text) + .then(() => { + showMessage('Kopiert!', `Text in Zwischenablage kopiert: ${text}`, levelId = -1, onclick = null, duration = 4000) + }) + .catch(err => { + console.error('Copy fehlgeschlagen', err); + }); +} + + +//#region Cookies +try { + // create or replace cookie + function setCookie(name, value) { + document.cookie=name + "=" + value + "; path=/; expires=" + (new Date(Number(new Date()) + (365 * 24 * 60 * 60 * 1000 * 10))).toGMTString(); + } + + // read cookie value + function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + } + + // remove cookie + function deleteCookie(name) { + if(getCookie(name) ) { + document.cookie = `${name}= ;expires=Thu, 01 Jan 1970 00:00:01 GMT`; + } + } +} catch( err ) { + alert(err); +} +//#endregion + + +let savedFontFamily = getCookie("fontfamily") || 'Arial'; +let savedFontSize = getCookie("fontsize") || 14; +let savedTheme = getCookie('theme') || 'dark'; + + +//#region Format +function formatHtml(text) { + return text?.replace(/\r?\n/g, '
').replace(/\t/g, '    ') +} + + +function dateFormat(date, format = null) { + if(date == null) return ""; + format = (format == null ? "dd.mm.yyyy HH:MM:SS" : format); + const finish_date = new Date(date); + + return format + .replace('yyyy', finish_date.getFullYear()) + .replace('yy', finish_date.getFullYear().toString().slice(-2)) + .replace('mm', ("0" + (finish_date.getMonth() + 1)).slice(-2)) + .replace('dd', ("0" + finish_date.getDate()).slice(-2)) + .replace('HH', ("0" + finish_date.getHours()).slice(-2)) + .replace('MM', ("0" + finish_date.getMinutes()).slice(-2)) + .replace('SS', ("0" + finish_date.getSeconds()).slice(-2)); +} + + +function isNumeric(value) { + return typeof value === "string" + && value.trim() !== "" + && !Number.isNaN(Number(value)); +} + + +function numberToCurrency(value, locale = 'de-DE', currency = 'EUR') { + return new Intl.NumberFormat(locale, { + style: 'currency', + currency: currency + }).format(value); +} +//#endregion + + +//#region Custom style sheets +async function loadCSSVars(path) { + try { + const res = await fetch(path); + const vars = await res.json(); + const root = document.documentElement; + + function setVars(obj, prefix = '') { + for (const [key, value] of Object.entries(obj)) { + if (typeof value === 'object') { + setVars(value, `${prefix}${key}-`); + } else { + root.style.setProperty(`--${prefix}${key}`, value); + } + } + } + setVars(vars); + } catch(err) { + alert(err) + } +} +//#endregion + + +//#region Dropzones + +/** + * HOWTO USE: + * const dz = makeElementDropzone(tr, { + * accept: ["image/png", "image/jpeg"], + * maxFiles: 5, + * maxFileSizeMB: 5, + * onDrop: files => console.log(files) + * }); + * @param {*} container + * @param {*} options + * @returns + */ +function makeElementDropzone(el, { + accept = [], + maxFiles = Infinity, + maxFileSizeMB = Infinity, + onDrop + }, allow = true) { + el.addEventListener('dragover', e => { + e.preventDefault(); + el.classList.add(allow ? 'drop-hover' : 'no-drop-hover'); + }); + + el.addEventListener('dragleave', () => { + el.classList.remove(allow ? 'drop-hover' : 'no-drop-hover'); + }); + + el.addEventListener('drop', e => { + e.preventDefault(); + if(allow) { + el.classList.remove('drop-hover'); + + const files = Array.from(e.dataTransfer.files); + if (!files.length) return; + + const validFiles = []; + + for (const file of files) { + if (accept.length && !accept.includes(file.type)) { + alert(`${file.name}: Dateityp nicht erlaubt`); + continue; + } + if (file.size > maxFileSizeMB * 1024 * 1024) { + alert(`${file.name}: Datei zu groß`); + continue; + } + validFiles.push(file); + if (validFiles.length >= maxFiles) break; + } + + if (validFiles.length && typeof onDrop === 'function') { + onDrop(validFiles); + } + } + }); +} + + +/** + * HOWTO USE: + * const dz = createDropzone(document.getElementById("dropzone"), { + * accept: ["image/png", "image/jpeg"], + * maxFiles: 5, + * maxFileSizeMB: 5, + * upload: file => fakeUpload(file), // optional + * onChange: files => console.log(files) + * }); + * @param {*} container + * @param {*} options + * @returns + */ +function createDropzone(container, options = {}) { + const { + accept = [], + maxFiles = Infinity, + maxFileSizeMB = Infinity, + upload = null, + onChange = () => {} + } = options; + + let files = []; + + // ---------- DOM ---------- + const wrapper = document.createElement('div'); + wrapper.className = 'dropzone-area'; + + const dropzone = document.createElement("div"); + dropzone.className = "dropzone"; + dropzone.textContent = "Dateien hier ablegen oder klicken"; + + const input = document.createElement("input"); + input.type = "file"; + input.hidden = true; + input.multiple = true; + input.accept = accept.join(","); + + const list = document.createElement("ul"); + list.className = "file-list"; + + wrapper.append(dropzone, input, list); + container.append(wrapper); + + // ---------- Helpers ---------- + const isDuplicate = file => + files.some(f => f.name === file.name && f.size === file.size); + + const isValid = file => { + if (maxFiles !== undefined && files.length >= maxFiles) return "Maximale Dateianzahl erreicht"; + if (maxFileSizeMB !== undefined && file.size > maxFileSizeMB * 1024 * 1024) + return `Max. ${maxFileSizeMB}MB erlaubt`; + if (accept !== undefined && accept.length && !accept.includes(file.type)) + return "Dateityp nicht erlaubt"; + if (isDuplicate(file)) return "Datei bereits vorhanden"; + return null; + }; + + const addFiles = selected => { + Array.from(selected).forEach(file => { + const error = isValid(file); + if (!error) { + files.push({ file, progress: 0 }); + if (upload) startUpload(file); + } else { + alert(`${file.name}: ${error}`); + } + }); + render(); + onChange(getFiles()); + }; + + const removeFile = index => { + files.splice(index, 1); + render(); + onChange(getFiles()); + }; + + // ---------- Upload ---------- + function startUpload(file) { + const entry = files.find(f => f.file === file); + upload(file, p => { + entry.progress = p; + render(); + }); + } + + // ---------- Drag Reorder ---------- + let dragIndex = null; + + function handleDragStart(i) { + dragIndex = i; + } + + function handleDrop(i) { + const [moved] = files.splice(dragIndex, 1); + files.splice(i, 0, moved); + dragIndex = null; + render(); + onChange(getFiles()); + } + + // ---------- Render ---------- + function render() { + list.innerHTML = ""; + + files.forEach((item, index) => { + const li = document.createElement("li"); + li.draggable = true; + + li.addEventListener("dragstart", () => handleDragStart(index)); + li.addEventListener("dragover", e => e.preventDefault()); + li.addEventListener("drop", () => handleDrop(index)); + + li.innerHTML = ` + ${item.file.name} + ${upload !== null ? `` : ''} + ${(item.file.size / 1024 / 1000).toFixed(2)} MB + + `; + + li.querySelector("button").onclick = () => removeFile(index); + list.appendChild(li); + }); + } + + // ---------- Events ---------- + dropzone.onclick = () => input.click(); + + input.onchange = e => { + addFiles(e.target.files); + input.value = ""; + }; + + dropzone.ondragover = e => { + e.preventDefault(); + dropzone.classList.add("active"); + }; + + dropzone.ondragleave = () => dropzone.classList.remove("active"); + + dropzone.ondrop = e => { + e.preventDefault(); + dropzone.classList.remove("active"); + addFiles(e.dataTransfer.files); + }; + + // ---------- API ---------- + const getFiles = () => files.map(f => f.file); + + return { + getFiles, + clear() { + files = []; + render(); + onChange([]); + } + }; +} +//#endregion + + + + +/* #region Multiselect textbox + + const multi = new MultiSelectTextbox({ + container: document.getElementById('users'), + source: [ + { id: 1, name: "Max" }, + { id: 2, name: "Anna" } + ], + keyFn: u => u.id, + valueFn: u => u.name + }); + + // API + multi.add({ id: 3, name: "Tom" }); + multi.removeByKey(1); + multi.set([ + { id: 1, name: "Max" }, + { id: 2, name: "Anna" } + ]); + console.log(multi.getValues()); + + // Event + document.getElementById('users') + .addEventListener('change', e => console.log(e.detail)); +*/ +class MultiSelectTextbox { + + constructor(options) { + this.container = options.container; + this.source = options.source ?? []; + this.allowNew = options.allowNew ?? true; + this.keyFn = options.keyFn ?? (item => item.key); + this.valueFn = options.valueFn ?? (item => item.value); + this.selected = options.selected ? [...options.selected] : []; + + this.init(); + } + + /* ================= INIT ================= */ + + init() { + this.inputWrapper = document.createElement('div'); + this.inputWrapper.className = 'mst-wrAberapper'; + this.container.appendChild(this.inputWrapper); + + this.input = document.createElement('input'); + this.input.type = 'text'; + this.input.className = 'mst-input'; + this.inputWrapper.appendChild(this.input); + + this.chipsContainer = document.createElement('div'); + this.chipsContainer.className = 'mst-chips-container'; + this.inputWrapper.appendChild(this.chipsContainer); + + this.dropdown = document.createElement('div'); + this.dropdown.className = 'mst-dropdown'; + this.dropdown.style.display = 'none'; + this.inputWrapper.appendChild(this.dropdown); + + this.input.addEventListener('input', () => this.updateDropdown()); + this.input.addEventListener('keydown', e => this.handleKey(e)); + this.input.addEventListener('dblclick', () => this.open()); + + document.addEventListener('click', e => { + if (!this.container.contains(e.target)) this.close(); + }); + + this.renderSelected(); + } + + /* ================= RENDER ================= */ + + renderSelected() { + this.chipsContainer.innerHTML = ''; + + this.selected.forEach(item => { + const chip = document.createElement('span'); + chip.className = 'mst-chip'; + chip.textContent = this.valueFn(item); + + const removeBtn = document.createElement('span'); + removeBtn.className = 'mst-chip-remove'; + removeBtn.textContent = '×'; + + removeBtn.onclick = () => { + this.removeByKey(this.keyFn(item)); + }; + + chip.appendChild(removeBtn); + this.chipsContainer.appendChild(chip); + }); + + this.emitChange(); + } + + /* ================= DROPDOWN ================= */ + + updateDropdown(query = this.input.value.toLowerCase()) { + query = query.toLowerCase(); + + const filtered = this.source.filter(item => { + const val = this.valueFn(item).toLowerCase(); + return val.includes(query) && + !this.selected.some(s => this.keyFn(s) === this.keyFn(item)); + }); + + if (filtered.length === 0 && !this.allowNew && !query) { + this.close(); + return; + } + + this.dropdown.innerHTML = ''; + + filtered.forEach(item => { + const div = document.createElement('div'); + div.className = 'mst-item'; + div.textContent = this.valueFn(item); + div.onclick = () => this.add(item); + this.dropdown.appendChild(div); + }); + + if (this.allowNew && query && !filtered.some(f => this.valueFn(f).toLowerCase() === query)) { + const div = document.createElement('div'); + div.className = 'mst-item new'; + div.textContent = `Neuer Wert: "${this.input.value}"`; + div.onclick = () => this.add({ key: query, value: this.input.value }); + this.dropdown.appendChild(div); + } + + this.dropdown.style.display = 'block'; + } + + handleKey(e) { + if (e.key === 'Enter') { + e.preventDefault(); + const first = this.dropdown.querySelector('.mst-item'); + if (first) first.click(); + } + else if (e.key === 'Backspace' && !this.input.value) { + this.selected.pop(); + this.renderSelected(); + } + } + + open() { + this.updateDropdown(''); + this.input.focus(); + } + + close() { + this.dropdown.style.display = 'none'; + } + + /* ================= PUBLIC API ================= */ + + getValues() { + return this.selected.map(s => ({ + key: this.keyFn(s), + value: this.valueFn(s) + })); + } + + add(item) { + const key = this.keyFn(item); + + if (this.selected.some(s => this.keyFn(s) === key)) + return; + + this.selected.push(item); + this.input.value = ''; + this.renderSelected(); + this.close(); + } + + set(items) { + this.selected = [...items]; + this.renderSelected(); + } + + removeByKey(key) { + this.selected = this.selected.filter(s => this.keyFn(s) !== key); + this.renderSelected(); + } + + clear() { + this.selected = []; + this.renderSelected(); + } + + setSource(items) { + this.source = [...items]; + this.updateDropdown(); + } + + addSource(items) { + if (!Array.isArray(items)) items = [items]; + + const existing = new Set(this.source.map(i => this.keyFn(i))); + + items.forEach(i => { + if (!existing.has(this.keyFn(i))) + this.source.push(i); + }); + + this.updateDropdown(); + } + + /* ================= EVENTS ================= */ + + emitChange() { + this.container.dispatchEvent(new CustomEvent('change', { + detail: this.getValues() + })); + } +} + +//#endregion + + + + + +/** + * e.g. client-side: + * + * @param {*} runtimeId + * @param {*} fn + */ +function registerWindowCleanup(runtimeId, fn) { + if (!windowCleanup.has(runtimeId)) { + windowCleanup.set(runtimeId, []); + } + windowCleanup.get(runtimeId).push(fn); +} + + + + +//#region Reload Plugin Script +function reloadPluginScript(src) { + try { + const old = document.querySelector(`script[src="${src}"]`); + if (old) old.remove(); + + const script = document.createElement("script"); + script.src = src + "?t=" + Date.now(); + script.defer = true; + document.body.appendChild(script); + } catch(err) { + alert(err); + } +} +//#endregion + + //#region ToolTip +// +// Sehr langer Text +// + +// +// ℹ️ +// +const tooltip = document.createElement('div'); +tooltip.className = 'global-tooltip'; +document.body.appendChild(tooltip); + +let activeEl = null; +let storedTitle = null; + +function positionTooltip(e) { + const offset = 20; + let x = e.clientX + offset; + let y = e.clientY + offset; + + const rect = tooltip.getBoundingClientRect(); + + if (x + rect.width > window.innerWidth) { + x = e.clientX - rect.width - offset; + } + + if (y + rect.height > window.innerHeight) { + y = e.clientY - rect.height + offset + 8; + } + + if (x < 0) x = 0; + if (y < 0) y = 0; + + tooltip.style.left = x + 'px'; + tooltip.style.top = y + 'px'; +} + +document.addEventListener('mouseover', e => { + const el = e.target.closest('[data-tooltip]'); + if (!el) return; + + const isTableCell = el.tagName === 'TD' || el.tagName === 'TH'; + const mode = el.dataset.tooltipMode ?? (isTableCell ? 'ellipsis' : 'always'); + + if (mode === 'ellipsis' && el.scrollWidth <= el.clientWidth) return; + + activeEl = el; + + storedTitle = el.getAttribute('title'); + if (storedTitle !== null) el.removeAttribute('title'); + + tooltip.innerHTML = el.dataset.tooltip; + tooltip.classList.add('visible'); + + // 🔥 SOFORT richtig positionieren + positionTooltip(e); +}); + +document.addEventListener('mousemove', e => { + if (!activeEl) return; + positionTooltip(e); +}); + +document.addEventListener('mouseout', e => { + if (!activeEl) return; + + if (!activeEl.contains(e.relatedTarget)) { + if (storedTitle !== null) activeEl.setAttribute('title', storedTitle); + + activeEl = null; + storedTitle = null; + tooltip.classList.remove('visible'); + } +}); +//#endregion + + + + +//#region Virtual table dataset +/* + const vt = virtualTable({ + tableEl: tableCAT, + data: [], + groupKey: 'Initiale', + rowHeight: 20, + buffer: 5, + filterConfig:{ + exceptedColumns: ['Aktiv', 'ID', 'Anhänge'], + columnModes:{ + Aktiv: 'dropdown', + Benutzername:'text', + 'Abteilung 3':'dropdown', + Genehmiger:'dropdown', + Bedarfsmelder:'dropdown', + Prioliste:'dropdown', + Einkauf: 'dropdown', + 'Admin':'dropdown', + 'Mail Aktiv':'dropdown' + }, + checkboxFilter: { + column: 'Aktiv', + rules: [ + { label: 'Aktiv: 🟢', test: v => v === true }, + { label: '🔴', test: v => v === false } + ] + } + }, + customRender: (row, tr) => { + createTd(tr, row['Aktiv'] ? '🟢' : '🔴', 'center'); + createTd(tr, row['User_ID'], 'left'); + createTd(tr, row['Benutzer'], 'left'); + createTd(tr, row['BenutzerName'], 'left'); + createTd(tr, row['Bedarfsmelder'], 'center'); + createTd(tr, row['Genehmiger'], 'center'); + createTd(tr, row['Abteilung 3'], 'center'); + createTd(tr, row['Prioliste'], 'center'); + createTd(tr, row['Einkauf'], 'center'); + createTd(tr, row['Admin'], 'center'); + createTd(tr, row['Mail Aktiv'], 'center'); +/* + createTd(tr, row['Gewerke'], 'center'); + createTd(tr, row['Orgs'], 'center'); + createTd(tr, row['Mail Benachrichtigungen'], 'left'); + } + }); +*/ +function virtualTable({ + tableEl, + data = [], + estimateHeight = 40, + buffer = 6, + groupKey = null, + rowKey = "id", + customRender = null, + filterConfig = null, + exceptedColumns = [] +}) { + let currentGroupKey = groupKey; + let virtualData = []; + let visibleRows = []; + const rowHeights = []; + const prefix = []; + const groupCollapse = new Map(); + + const filterState = { + selectedColumn: '', + searchText: '', + dropdownValue: '', + dropdownCache: {}, + checkbox: new Set() + }; + + //-------------------------------------------- + // Scroll Parent + //-------------------------------------------- + function getScrollParent(el) { + let p = el.parentElement; + while (p) { + const s = getComputedStyle(p); + if (/(auto|scroll)/.test(s.overflowY)) return p; + p = p.parentElement; + } + return document.scrollingElement; + } + const wrapper = getScrollParent(tableEl); + + + function syncFilterWidth(container){ + function update(){ + const rect = wrapper.getBoundingClientRect(); + + container.style.width = (rect.width - 11) + "px"; + container.style.left = "0px"; + } + + update(); + wrapper.addEventListener('scroll', update, {passive:true}); + } + + + //-------------------------------------------- + // Gruppen vorbereiten + //-------------------------------------------- + function prepareData() { + virtualData = []; + + if (!currentGroupKey) { + virtualData = data.map(r => ({...r, isGroupHeader:false})); + } else { + const groups = {}; + data.forEach(r=>{ + const key = r[currentGroupKey] ?? "Keine Gruppe"; + (groups[key]??=[]).push(r); + }); + + Object.keys(groups).forEach(key=>{ + const gIndex = virtualData.length; + const collapsed = groupCollapse.get(key) || false; + + virtualData.push({isGroupHeader:true, groupKey:key, collapsed}); + + groups[key].forEach(r=>{ + virtualData.push({...r,isGroupHeader:false,groupIndex:gIndex}); + }); + }); + } + + applyFilters(); + rebuildPrefix(); + } + + //-------------------------------------------- + // Filter auf virtuelle Daten anwenden + //-------------------------------------------- + function applyFilters() { + visibleRows = []; + let visibleCount = 0; + + // alle vorherigen Filterzustände zurücksetzen + virtualData.forEach(row => row._filteredOut = false); + + virtualData.forEach(row => { + if (row.isGroupHeader) { + visibleRows.push(row); + return; + } + + // Spaltenfilter + if (filterState.selectedColumn) { + const val = row[filterState.selectedColumn]; + const mode = filterConfig.columnModes[filterState.selectedColumn] || 'text'; + if (mode === 'text' && filterState.searchText) { + if (!val?.toString().toLowerCase().includes(filterState.searchText)) { + row._filteredOut = true; + } + } + if (mode === 'dropdown' && filterState.dropdownValue) { + if (val?.toString() !== filterState.dropdownValue) row._filteredOut = true; + } + } else if (filterState.searchText) { + // globale Suche + const match = Object.keys(row).some(k => { + if(k==='isGroupHeader'||k==='groupIndex') return false; + return row[k]?.toString().toLowerCase().includes(filterState.searchText); + }); + if (!match) row._filteredOut = true; + } + + // Checkbox-Filter + if (filterConfig?.checkboxFilter && filterState.checkbox.size > 0) { + const val = row[filterConfig?.checkboxFilter.column]; + const pass = [...filterState.checkbox].some(r => r.test(val)); + if(!pass) row._filteredOut = true; + } + + // Gruppierte Rows berücksichtigen + const parent = virtualData[row.groupIndex]; + if (parent?.collapsed || row._filteredOut) return; + + visibleRows.push(row); + visibleCount++; + }); + + // Live-Counter + if(filterState.counterEl) filterState.counterEl.textContent = `${visibleCount} Treffer`; + } + + + + //-------------------------------------------- + // Visible rows prefix sums + //-------------------------------------------- + function rebuildPrefix(){ + prefix.length = visibleRows.length+1; + prefix[0]=0; + + for(let i=0;i>1; + if(prefix[mid] { + dots = (dots + 1) % 4; // 0..3 Punkte + td.textContent = "Lade Daten" + ".".repeat(dots); + }, 500); + + return; + } + + // Wenn Daten vorhanden sind, Animation stoppen + clearInterval(loadingInterval); + + const start = Math.max(0, findStart(scrollTop) - buffer); + let end = start; + while (end < visibleRows.length && prefix[end] - prefix[start] < viewport + buffer * estimateHeight) + end++; + + tbody.innerHTML = ''; + + // top spacer + const top = document.createElement("tr"); + top.style.height = prefix[start] + "px"; + tbody.appendChild(top); + + const frag = document.createDocumentFragment(); + + for (let i = start; i < end; i++) { + const row = visibleRows[i]; + const tr = document.createElement("tr"); + tr.dataset.index = i; + + if (row.isGroupHeader) { + tr.className = "grouprow"; + + const td = document.createElement("td"); + td.colSpan = 100; + + const toggle = document.createElement("span"); + toggle.textContent = row.collapsed ? '►' : '▼'; + toggle.style.cursor = "var(--theme-cursor-pointer) -16 16 , pointer"; + + const toggleFn = () => { + row.collapsed = !row.collapsed; + groupCollapse.set(row.groupKey, row.collapsed); + applyFilters(); + rebuildPrefix(); + render(); + }; + + toggle.onclick = toggleFn; + tr.ondblclick = toggleFn; + + td.append(toggle, " ", row.groupKey); + tr.appendChild(td); + + } else { + if (customRender) customRender(row, tr); + else { + Object.keys(row).forEach(k => { + if (k === "isGroupHeader" || k === "groupIndex") return; + const td = document.createElement("td"); + td.textContent = row[k]; + tr.appendChild(td); + }); + } + } + + frag.appendChild(tr); + } + + tbody.appendChild(frag); + + // bottom spacer + const bottom = document.createElement("tr"); + bottom.style.height = (prefix[visibleRows.length] - prefix[end]) + "px"; + tbody.appendChild(bottom); + + measureRows(); + } + + //-------------------------------------------- + // echte Höhen messen + //-------------------------------------------- + function measureRows(){ + const trs=tableEl.querySelectorAll("tbody tr[data-index]"); + + let changed=false; + + trs.forEach(tr=>{ + const i=Number(tr.dataset.index); + const h=tr.getBoundingClientRect().height; + if(rowHeights[i]!==h){ + rowHeights[i]=h; + changed=true; + } + }); + + if(changed){ + rebuildPrefix(); + requestAnimationFrame(render); + } + } + + //-------------------------------------------- + // Scroll & Resize + //-------------------------------------------- + wrapper.addEventListener("scroll",()=>requestAnimationFrame(render)); + new ResizeObserver(()=>requestAnimationFrame(render)).observe(wrapper); + +// const wrapper = getScrollParent(tableEl); + + +// Optional: auch Fenstergröße überwachen +window.addEventListener('resize', () => { + rebuildPrefix(); + requestAnimationFrame(render); +}); + + + + //-------------------------------------------- + // Init Filter-UI + //-------------------------------------------- + function initFilterUI() { + if(!filterConfig) return; + + const container = document.createElement('div'); + container.className = 'table-filter-container'; + container.style.position = 'sticky'; + container.style.top = '0px'; + container.style.zIndex = 20; + tableEl.before(container); + + + // Höhe messen → thead offset setzen + requestAnimationFrame(()=>{ + const h = container.getBoundingClientRect().height; + tableEl.style.setProperty('--filter-height', h + 'px'); + }); + + filterState.counterEl = document.createElement('div'); + filterState.counterEl.className = 'live-counter'; + container.appendChild(filterState.counterEl); + syncFilterWidth(container); + + const wrapperResizeObserver = new ResizeObserver(() => { + // Filter-Header anpassen + syncFilterWidth(container); + + // Tabelle neu rendern + rebuildPrefix(); + render(); + }); + wrapperResizeObserver.observe(wrapper); + + const wrap = document.createElement('div'); + wrap.style="display:flex;gap:10px;align-items:center"; + + // Spaltenauswahl + const colSelect = document.createElement('select'); + const emptyOpt = document.createElement('option'); + emptyOpt.value=''; emptyOpt.textContent='-- Alles --'; + colSelect.appendChild(emptyOpt); + tableEl.querySelectorAll('thead th').forEach(th=>{ + if(!filterConfig?.exceptedColumns?.includes(th.textContent.trim())){ + const opt = document.createElement('option'); + opt.value=th.textContent.trim(); + opt.textContent=th.textContent.trim(); + colSelect.appendChild(opt); + } + }); + wrap.appendChild(colSelect); + + // Textinput + const input = document.createElement('input'); + input.type='text'; + wrap.appendChild(input); + + // Dropdown (hidden by default) + const select = document.createElement('select'); + select.style.display='none'; + wrap.appendChild(select); + + // Event für Spaltenwechsel + colSelect.addEventListener('change', () => { + filterState.selectedColumn = colSelect.value; + filterState.searchText = ''; + filterState.dropdownValue = ''; + input.value = ''; + select.value = ''; + + if (!filterState.selectedColumn) { + input.style.display = ''; + select.style.display = 'none'; + } else { + const mode = filterConfig.columnModes[filterState.selectedColumn]; + if (mode === 'text') { + input.style.display = ''; + select.style.display = 'none'; + } else if (mode === 'dropdown') { + input.style.display = 'none'; + select.style.display = ''; + populateDropdownForColumn(filterState.selectedColumn); // alle Werte aus virtualData + } + } + + // **Virtuelle Daten filtern** + applyFilters(); + render(); + }); + + + input.addEventListener('input',()=>{ filterState.searchText = input.value.toLowerCase(); applyFilters(); rebuildPrefix(); measureRows(); render(); }); + select.addEventListener('change',()=>{ filterState.dropdownValue = select.value; applyFilters(); rebuildPrefix(); measureRows(); render(); }); + + container.appendChild(wrap); + + // Checkbox-Filter + if (filterConfig?.checkboxFilter) { + const cfgCheck = filterConfig?.checkboxFilter; + const cbWrapper = document.createElement('div'); + cbWrapper.style.display = "flex"; + cbWrapper.style.gap = "12px"; + cbWrapper.style.alignItems = "center"; + + cfgCheck.rules.forEach(rule => { + + const checkWrapper = document.createElement('label'); + const checkInput = document.createElement('input'); + const checkTrack = document.createElement('span'); + const checkThumb = document.createElement('span'); + + // Deine Klassen + checkWrapper.classList.add("cb", "cb-switch"); + checkInput.type = 'checkbox'; + + checkTrack.classList.add('switch-track'); + checkTrack.setAttribute("aria-hidden", 'true'); + + checkThumb.classList.add('switch-thumb'); + checkThumb.setAttribute("aria-hidden", 'true'); + + // Verhalten + checkInput.addEventListener('change', () => { + if (checkInput.checked) filterState.checkbox.add(rule); + else filterState.checkbox.delete(rule); + + applyFilters(); + rebuildPrefix(); + render(); + }); + + + // Aufbau (wichtig: input VOR track für CSS :checked + .switch-track) + checkTrack.appendChild(checkThumb); + checkWrapper.append(rule.label, checkInput, checkTrack); + cbWrapper.appendChild(checkWrapper); + }); + + container.appendChild(cbWrapper); + } + + + function populateDropdownForColumn(colName){ + // Alle Werte aus der virtuellen Tabelle sammeln + const values = new Set( + virtualData + .filter(r => !r.isGroupHeader) // nur echte Datenzeilen + .map(r => r[colName]) + ); + + // sortieren + const sorted = [...values].sort((a,b)=> a.toString().localeCompare(b.toString())); + + // Options bauen + select.innerHTML = '' + + sorted.map(v=>``).join(''); + } + + } + + //-------------------------------------------- + // Init + //-------------------------------------------- + prepareData(); + render(); + initFilterUI(); + + return { + addData(newData){ + if (!newData) return; + + // JSON-Support + if (typeof newData === "string") { + try { newData = JSON.parse(newData); } catch { return; } + } + + const incoming = Array.isArray(newData) ? newData : [newData]; + + // Upsert nach rowKey + if (!Array.isArray(data) || data.length === 0) { + data = incoming.slice(); + } else { + const indexMap = new Map(); + data.forEach((row,i)=>{ + const key = row?.[rowKey]; + if(key != null) indexMap.set(key,i); + }); + + incoming.forEach(row=>{ + const key = row?.[rowKey]; + if(key != null && indexMap.has(key)){ + data[indexMap.get(key)] = row; // ersetzen + } else { + data.push(row); // anhängen + } + }); + } + + //---------------------------------------- + // Jetzt alle Filter korrekt anwenden + //---------------------------------------- + // virtualData neu vorbereiten + prepareData(); // virtualData wird neu gebaut + + // Dropdown-Werte für ausgewählte Spalte aktualisieren + if(filterState.selectedColumn && filterConfig?.columnModes[filterState.selectedColumn] === 'dropdown'){ + populateDropdownForColumn(filterState.selectedColumn); + } + + // _filteredOut zurücksetzen UND Filter neu anwenden + applyFilters(); + rebuildPrefix(); + render(); // alles rendern + }, + removeData(target) { + if (!target) return; + + //---------------------------------------- + // Helper: normalize targets + //---------------------------------------- + let matcher; + + // Funktion → direkt verwenden + if (typeof target === "function") { + matcher = target; + } + // Array → mehrere IDs oder Objekte + else if (Array.isArray(target)) { + const keys = new Set( + target.map(t => typeof t === "object" ? t[rowKey] : t) + ); + matcher = row => keys.has(row[rowKey]); + } + // Objekt → anhand rowKey + else if (typeof target === "object") { + matcher = row => row[rowKey] === target[rowKey]; + } + // Primitive → ID + else { + matcher = row => row[rowKey] === target; + } + + //---------------------------------------- + // Daten filtern + //---------------------------------------- + data = data.filter(row => !matcher(row)); + + //---------------------------------------- + // Rebuild + Render + //---------------------------------------- + prepareData(); + + // Dropdown neu füllen (falls aktiv) + if ( + filterState.selectedColumn && + filterConfig?.columnModes[filterState.selectedColumn] === 'dropdown' + ) { + populateDropdownForColumn(filterState.selectedColumn); + } + + applyFilters(); + rebuildPrefix(); + render(); + }, + setGroupBy(newKey){ + currentGroupKey = newKey; + groupCollapse.clear(); // optional: alte Collapse-Zustände löschen + prepareData(); + render(); + }, + refresh(){ applyFilters(); render(); }, + clearData() { data = [] }, + source(newData) { data = []; this.addData(newData); }, + prepareData() { prepareData(); } + }; +} +//#endregion + + +//#region Table cells +function createTd(tr, text, options = {}) { + const { + id = null, + hidden = false, + classes = [], + styles = null, + attributes = {}, + onclick = null + } = options; + + const td = document.createElement('td'); + + td.innerHTML = text ?? ''; + + if(id !== null) { + td.id = id; + } + + // Standard-Stile & Attribute + td.hidden = hidden; + + classes.forEach(c => td.classList.add(c)); + + if (styles && typeof styles === 'object') { + Object.entries(styles).forEach(([prop, value]) => { + td.style[prop] = value; + }); + } + + // Data attributes + if (attributes && typeof attributes === 'object') { + Object.entries(attributes).forEach(([key, value]) => { + td.setAttribute(key, value); + }); + } + + if(onclick !== undefined) { + td.addEventListener('click', evt => { + if( typeof onclick === 'function' ) onclick(); + evt.preventDefault(); + }); + } + + tr.appendChild(td); + requestAnimationFrame(() => { + if (td.scrollWidth > td.clientWidth) { + td.title = td.textContent.trim(); + } + }); + return td; +}; +//#endregion + +//#region Tabs +function createTab(tabSelector, name, onclick = null) { + const tabElement = document.createElement('div'); + tabElement.className = 'tab'; + tabElement.dataset.tab = name; + tabElement.textContent = name; + tabSelector.appendChild(tabElement); + tabElement.addEventListener('click', async () => { + Array.from(document.querySelectorAll('.tab')).forEach(t => t.classList.remove('active')); + tabElement.classList.add('active'); + if(typeof onclick === 'function') { + onclick(); + } + }); +} +//#endregion \ No newline at end of file diff --git a/public/javascript/notifyBubble.js b/public/javascript/notifyBubble.js new file mode 100644 index 0000000..94fc261 --- /dev/null +++ b/public/javascript/notifyBubble.js @@ -0,0 +1,157 @@ +class NotifyBubble { + constructor(trayButton, bubbleSelector) { + this.button = trayButton; + this.bubble = document.querySelector(bubbleSelector); + this.hideTimeout = null; + this.initEvents(); + this.initExistingItems(); + this.updateCounter(); // 🔥 initialer Counter + } + + /* ========================= + SHOW / HIDE + ========================== */ + show() { + clearTimeout(this.hideTimeout); + this.button.classList.add("active"); + + // Tooltip entfernen beim Öffnen + this.button.removeAttribute('data-tooltip'); + } + + hide(delay = 1000) { + clearTimeout(this.hideTimeout); + + this.hideTimeout = setTimeout(() => { + this.button.classList.remove("active"); + + // 🔥 Counter nach Schließen anzeigen + this.updateCounter(); + + }, delay); + } + + /* ========================= + COUNTER / TOOLTIP + ========================== */ + updateCounter() { + const items = this.bubble.querySelectorAll(".bubble-item"); + const count = items.length; + + if (!this.button.classList.contains("active") && count > 0) { + this.button.setAttribute('data-tooltip', `${count} neue Benachrichtigung${count > 1 ? 'en' : ''}`); + } else { + this.button.setAttribute('data-tooltip', "Keine Benachrichtigungen"); + } + } + + /* ========================= + EVENTS + ========================== */ + initEvents() { + // Toggle bei Klick auf Button + this.button.addEventListener("click", (e) => { + e.stopPropagation(); + + if (this.button.classList.contains("active")) { + this.hide(0); + } else { + this.show(); + } + }); + + // Klick innerhalb der Bubble soll NICHT schließen + this.bubble.addEventListener("click", (e) => { + e.stopPropagation(); + }); + + // Klick irgendwo anders → schließen + document.addEventListener("click", () => { + if (this.button.classList.contains("active")) { + this.hide(0); + } + }); + } + + /* ========================= + INIT EXISTING ITEMS + ========================== */ + initExistingItems() { + this.bubble.querySelectorAll(".bubble-item").forEach(item => { + this.attachItemEvents(item); + }); + } + + /* ========================= + ITEM EVENTS + ========================== */ + attachItemEvents(item) { + item.addEventListener("click", () => { + const checkbox = item.querySelector("input[type='checkbox']"); + if (checkbox) { + checkbox.checked = !checkbox.checked; + + fetch('/api/NotifyTray/markAsSeen', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id: item.dataset.id, + value: checkbox.checked + }) + }); + + // 🔥 Counter ggf. aktualisieren + this.updateCounter(); + } + }); + } + + /* ========================= + ADD SINGLE ITEM + ========================== */ + addItem(notification) { + const item = document.createElement("label"); + item.className = "bubble-item"; + item.dataset.id = notification.ID; + + item.innerHTML = ` +
${notification.Message}
+ +
+
+
${dateFormat(notification.CreatedAt, 'dd.mm.yyyy HH:MM:SS')}
+
${notification.PluginName}
+
+
+ +
+
+ `; + + this.attachItemEvents(item); + this.bubble.appendChild(item); + + // 🔥 Counter aktualisieren + this.updateCounter(); + } + + /* ========================= + ADD MULTIPLE ITEMS + ========================== */ + addItems(notifications) { + notifications.forEach(n => this.addItem(n)); + } + + /* ========================= + CLEAR + ========================== */ + clear() { + this.bubble.innerHTML = ""; + this.updateCounter(); + } +} \ No newline at end of file diff --git a/public/javascript/os.js b/public/javascript/os.js new file mode 100644 index 0000000..1a7c8ea --- /dev/null +++ b/public/javascript/os.js @@ -0,0 +1,1081 @@ +try { +const isMobile = ( + window.matchMedia("(pointer: coarse)").matches || + window.innerWidth <= 768 || + /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) +); + +let topZ = 100; +const MAX_PADDING = { left: 8, top: 8, right: 8, bottom: 60 }; +const startBtn = document.getElementById('start-btn'); +const startMenu = document.getElementById('start-menu'); +const windowsContainer = document.getElementById('windows'); +const taskbarWindows = document.getElementById('taskbar-windows'); +const ctx = new ContextMenu(); +const windowCleanup = new Map(); +const username = getCookie('sAMAccountName'); +const LS_KEY = (key) => `${username}:${key}`; + +startBtn.addEventListener('click', (evt) => { + evt.stopPropagation(); // verhindert sofortiges Schließen + startMenu.classList.toggle('hidden'); +}); + +// Launch app when clicking start menu item +document.addEventListener('click', async (e) => { + const target = e.target.closest('.start-item'); + const clickedInsideMenu = startMenu.contains(e.target); + const clickedButton = startBtn.contains(e.target); + + if(!clickedInsideMenu && !clickedButton) { + startMenu.classList.add('hidden'); + return; + } + if (!target) return; + + const name = target.dataset.appname; + const view = target.dataset.appview; + + + const id = `win-${name}.${view}`; + const active = target.dataset.active; + + if(active !== "true") return + // 👉 WICHTIG: erst prüfen ob Fenster existiert + const focused = focusWindowById(id); + if (focused) { + startMenu.classList.add('hidden'); + return; + } + + openApp({ + name, + view, + viewLabel: target.dataset.viewlabel + }); + + startMenu.classList.add('hidden'); +}); + + +/* + // serverside route + app.post('/children/Demo/second_frame', async (req, res) => { + await renderView(app, `${metadata.name}/views/chldren/%name_of_file%`, { ...metadata, tutorial: false }, res) + }); + + //clientside trigger + e.g.: onClick: () => openView({ name: 'Demo', view: 'second_frame', viewLabel: 'Second Frame', content: 'Hello world, I\'m the second frame', defaultSize: { width: '900px', height: '300px' } }) +*/ +window.openView = async (payload) => { + const { name, view, viewLabel } = payload; + const id = `win-${name}.${view}`; + const exists = document.querySelector(`[data-winid="${id}"]`); + + let resume = null; + + if (exists) { + resume = { + left: exists.style.left, + top: exists.style.top + }; + exists.remove(); + } + + const res = await fetch(`/children/${name}/${view}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!res.ok) { + console.warn('Child partial nicht gefunden', `${name}.${view}`); + return; + } + + const html = await res.text(); + + const wrapper = document.createElement('div'); + wrapper.innerHTML = html; + + const win = wrapper.firstElementChild; + + win.style.left = resume?.left || (50 + Math.random()*100) + 'px'; + win.style.top = resume?.top || (50 + Math.random()*60) + 'px'; + + const defaultW = payload.defaultSize?.width || 800; + const defaultH = payload.defaultSize?.height || 600; + + win.style.width = payload.size?.width || defaultW; + win.style.height = payload.size?.height || defaultH; + + win.dataset.winid = id; + win.dataset.appname = name; + win.dataset.appview = view; + win.dataset.viewLabel = viewLabel; + win.dataset.type = 'view'; + + windowsContainer.appendChild(win); + + bringToFront(win); + reloadJS(win); + wireWindowControls(win); +} + + +async function openApp(payload) { + const { name, view, viewLabel } = payload; + const saved = JSON.parse(localStorage.getItem(LS_KEY('openWindows')) || '[]') + + // 🔁 prüfen ob schon offen + const id = `win-${name}.${view}`; + const exists = document.querySelector(`[data-winid="${id}"]`); + if (exists) { + bringToFront(exists); + startMenu.classList.add('hidden'); + return; + } + + // 📡 Server holen + const resMeta = await fetch('/api/open_app', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (!resMeta.ok) { + console.warn('open_app failed'); + return; + } + + const meta = await resMeta.json(); + const res = await fetch(`/window/${name}/${view}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!res.ok) { + console.warn('Window partial nicht gefunden', `${name}.${view}`); + return; + } + + const html = await res.text(); + + const wrapper = document.createElement('div'); + wrapper.innerHTML = html; + const win = wrapper.firstElementChild; + + // 📍 Position + win.style.left = meta.location?.left || (50 + Math.random()*100) + 'px'; + win.style.top = meta.location?.top || (50 + Math.random()*60) + 'px'; + + // 📐 Größe + const defaultW = meta.context.defaultSize?.width || 800; + const defaultH = meta.context.defaultSize?.height || 600; + + win.style.width = meta.size?.width || defaultW; + win.style.height = meta.size?.height || defaultH; + + // 🆔 IDs setzen + win.dataset.winid = id; + win.dataset.appname = name; + win.dataset.appview = view; + win.dataset.viewLabel = viewLabel; + win.dataset.type = 'app'; + + windowsContainer.appendChild(win); + + if (isMobile) { + const maximizeBtn = win.querySelector('.maximize'); + if (maximizeBtn) maximizeBtn.style.display = 'none'; + } + + if(!saved.length) { + payload.zIndex = topZ; + } + if (payload.state !== 'minimized') { + bringToFront(win); + } + + if (payload.state !== 'minimized') requestAnimationFrame(() => saveOpenWindows()); + reloadJS(win); + + // 🧷 Taskbar Button + const btn = document.createElement('div'); + btn.className = 'taskbar-item focus'; + btn.dataset.winid = id; + + btn.innerHTML = `${meta.context.menu.label}
${ + meta.context.menu.label != viewLabel ? `[${viewLabel}]` : ' ' + }`; + + btn.oncontextmenu = (evt) => { + evt.preventDefault(); + if(ctx != null) { + ctx.setItems([ { label: "Schließen", onClick: () => { + btn.remove(); + saveOpenWindows() + handleWindowAction({ + id: win.dataset.winid, + action: 'close' + }); + } } ]); + ctx.show(btn, { position: 'top' }); + } + } + + resetFocus(id); + taskbarWindows.appendChild(btn); + + wireWindowControls(win, btn); + restoreWindow(win, meta, btn); +} + + +function reloadJS(win) { + const runtimeId = win.dataset.runtimeId; + + win.querySelectorAll('script').forEach(oldScript => { + const script = document.createElement('script'); + + if (!oldScript.src) { + script.textContent = `(function(runtimeId){${oldScript.textContent}})("${runtimeId}");`; + } + + win.appendChild(script); + oldScript.remove(); + }); +} + +function handleWindowAction(payload) { + const win = document.querySelector(`[data-winid="${payload.id}"]`); + if (!win) return; + + const tb = document.querySelector(`.taskbar-item[data-winid="${payload.id}"]`); + + if (tb) resetFocus(payload.id); + + if (payload.action === 'minimize') { + win.dataset.state = 'minimized'; + win.style.display = 'none'; + tb?.classList.add('minimized'); + } + + if (payload.action === 'restore') { + win.style.display = 'flex'; + win.dataset.state = 'normal'; + bringToFront(win); + tb?.classList.remove('minimized'); + } + + if (payload.action === 'close') { + win.remove(); + tb?.remove(); + } + + saveOpenWindows(); +} + + // mainSocket.on('window_action', payload => { + // // simple forwarding; client-specific behaviour can be implemented + // const win = document.querySelector(`[data-winid="${payload.id}"]`); + // if (!win) return; + + // const tb = document.querySelector(`.taskbar-item[data-winid="${payload.id}"]`); + + // if(tb) { + // resetFocus(payload.id); + // } + + // if (payload.action === 'minimize') { + // win.dataset.state = 'minimized'; + // win.style.display = 'none'; + + // if (tb) { + // tb.classList.add('minimized'); + // } + + // saveOpenWindows(); // <-- HIER + // } + + // if (payload.action === 'restore') { + // win.style.display = 'flex'; + // win.dataset.state = 'normal'; + // bringToFront(); + // if (tb) { + // tb.classList.remove('minimized'); + // tb.classList.remove('focus'); + // } + + // saveOpenWindows(); // <-- HIER + // } + // if (payload.action === 'close') { + // win.remove(); + // if (tb) tb.remove(); + + // saveOpenWindows(); // <-- HIER + // } + // }); + + // taskbar click toggles minimize + // taskbarWindows.addEventListener('click', (e) => { + // const btn = e.target.closest('.taskbar-item'); + // if (!btn) return; + + // focusWindowById(btn.dataset.winid); + // }); + taskbarWindows.addEventListener('click', (e) => { + const btn = e.target.closest('.taskbar-item'); + if (!btn) return; + + const id = btn.dataset.winid; + const win = document.querySelector(`[data-winid="${id}"]`); + if (!win) return; + + const isHidden = win.style.display === 'none'; + const isFocused = btn.classList.contains('focus'); + + // 🔵 1. minimiert → restore + fokus + if (isHidden) { + win.style.display = 'flex'; + win.dataset.state = 'normal'; + + bringToFront(win); + + btn.classList.add('focus'); + btn.classList.remove('minimized'); + + saveOpenWindows(); + return; + } + + // 🟢 2. sichtbar + aktiv → MINIMIEREN + if (isFocused) { + win.style.display = 'none'; + win.dataset.state = 'minimized'; + + btn.classList.remove('focus'); + btn.classList.add('minimized'); + + saveOpenWindows(); + return; + } + + // 🟡 3. sichtbar aber nicht aktiv → FOKUS + bringToFront(win); + resetFocus(id); + + btn.classList.add('focus'); + btn.classList.remove('minimized'); +}); + + +function applyMaximized(win) { + win.style.left = MAX_PADDING.left + 'px'; + win.style.top = MAX_PADDING.top + 'px'; + win.style.width = `calc(100% - ${MAX_PADDING.left + (MAX_PADDING.right * 1.5)}px)`; + win.style.height = `calc(100% - ${MAX_PADDING.top + MAX_PADDING.bottom}px)`; + + win.classList.add('max'); + win.dataset.state = 'maximized'; +} + + + function focusWindowById(id) { + const win = document.querySelector(`[data-winid="${id}"]`); + if (!win) return false; + + const tb = document.querySelector(`.taskbar-item[data-winid="${id}"]`); + + // 1. Falls minimiert → wieder anzeigen + if (win.style.display === 'none') { + win.style.display = 'flex'; + win.dataset.state = 'normal'; + } + + // 2. Fokus setzen + bringToFront(win); + + if (tb) { + resetFocus(id); + tb.classList.add('focus'); + tb.classList.remove('minimized'); + } + + return true; + } + + + function cacheWindowGeometry(win) { + const rect = win.getBoundingClientRect(); + + win.dataset.lastGeometry = JSON.stringify({ + left: `${rect.left}px`, + top: `${rect.top}px`, + width: `${rect.width}px`, + height: `${rect.height}px` + }); + } + + + + function storeNormalGeometry(win) { + // ❌ NICHT speichern, wenn maximiert oder gesnappt + if (win.classList.contains('max')) return; + if (win.dataset.snapped) return; + + const r = win.getBoundingClientRect(); + win.dataset.normalGeometry = JSON.stringify({ + left: `${r.left}px`, + top: `${r.top}px`, + width: `${r.width}px`, + height: `${r.height}px`, + }); + } + + + function makeResizable(win) { + let resizing = false; + let dir, startX, startY, startW, startH, startL, startT; + + const minW = 300; + const minH = 200; + + win.querySelectorAll('.window-resize-handle').forEach(handle => { + handle.addEventListener('mousedown', startResize); + handle.addEventListener('touchstart', startResize, { passive: false }); + }); + + function startResize(e) { + e.preventDefault(); + if (win.classList.contains('max')) return; + + resizing = true; + dir = e.target.dataset.dir; + + const r = win.getBoundingClientRect(); + startX = e.clientX || e.touches[0].clientX; + startY = e.clientY || e.touches[0].clientY; + startW = r.width; + startH = r.height; + startL = r.left; + startT = r.top; + + document.addEventListener('mousemove', resize); + document.addEventListener('mouseup', stopResize); + document.addEventListener('touchmove', resize, { passive: false }); + document.addEventListener('touchend', stopResize); + } + + function resize(e) { + if (!resizing) return; + + const x = e.clientX || e.touches[0].clientX; + const y = e.clientY || e.touches[0].clientY; + const dx = x - startX; + const dy = y - startY; + + if (dir.includes('e')) { + win.style.width = Math.max(minW, startW + dx) + 'px'; + } + + if (dir.includes('s')) { + win.style.height = Math.max(minH, startH + dy) + 'px'; + } + + if (dir.includes('w')) { + const w = Math.max(minW, startW - dx); + win.style.width = w + 'px'; + win.style.left = startL + (startW - w) + 'px'; + } + + if (dir.includes('n')) { + const h = Math.max(minH, startH - dy); + win.style.height = h + 'px'; + win.style.top = startT + (startH - h) + 'px'; + } + } + + function stopResize() { + resizing = false; + document.removeEventListener('mousemove', resize); + document.removeEventListener('mouseup', stopResize); + document.removeEventListener('touchmove', resize); + document.removeEventListener('touchend', stopResize); + + storeNormalGeometry(win); // <-- HIER (neu) + saveOpenWindows(); + } + } + + + + function addResizeHandles(win) { + const dirs = ['n','s','e','w','ne','nw','se','sw']; + dirs.forEach(dir => { + const h = document.createElement('div'); + h.classList.add( + 'window-resize-handle', + `window-resize-${dir}` + ); + h.dataset.dir = dir; + win.appendChild(h); + }); + } + + + // Simple drag (titlebar) +function wireWindowControls(win, taskbarBtn = null) { + const minimize = win.querySelector('.minimize'); + const maximize = win.querySelector('.maximize'); + const close = win.querySelector('.close'); + const titlebar = win.querySelector('.window-titlebar'); + + function isControl(el) { + return el.closest('.minimize, .maximize, .close, .window-resize-handle'); + } + + + addResizeHandles(win); + makeResizable(win); + + // 🟦 Minimieren + if(minimize != null) { + minimize.addEventListener('click', () => { + cacheWindowGeometry(win); // 🔥 WICHTIG + win.dataset.state = 'minimized'; + win.style.display = 'none'; + + if (taskbarBtn) { + taskbarBtn.classList.add('minimized'); + taskbarBtn.classList.remove('focus'); + } + + saveOpenWindows(); + }); + + } + + win.addEventListener('mousedown', (e) => { + if (isControl(e.target)) return; + if (win.style.display !== 'none') { + bringToFront(win); + } + }); + + win.addEventListener('touchstart', () => { + if (win.style.display !== 'none') bringToFront(win); + }); + + + // 🟩 Maximieren / Wiederherstellen + if(maximize != null) { + maximize.addEventListener('click', (evt) => { + if(taskbarBtn != null) { + taskbarBtn.classList.add('focus'); + maximize.innerHTML = win.dataset.state === 'normal' ? '🗗' : '▢'; + resetFocus(win.dataset.winid); + } + toggleMaximize(); + saveOpenWindows() + }); + } + + // 🟥 Schließen + if(close != null) { + close.addEventListener('click', () => { + destroyWindow(win); + if (taskbarBtn) taskbarBtn.remove(); + saveOpenWindows(); + handleWindowAction({ + id: win.dataset.winid, + action: 'close' + }); + }); + } + + // 🟨 Doppelklick auf Titelleiste → Maximieren / Wiederherstellen + let lastTap = 0; + + if (taskbarBtn !== null) { + titlebar.addEventListener('dblclick', (evt) => { + toggleMaximize(); + saveOpenWindows() + }); + } + + titlebar.addEventListener('touchend', (ev) => { + const current = Date.now(); + const delta = current - lastTap; + + if (delta < 300 && delta > 0) { + // Double tap → maximize / restore + toggleMaximize(); + } + lastTap = current; + }); + + +function destroyWindow(win) { + try { + const runtimeId = win.dataset.runtimeId; + + // Cleanup ausführen + if(windowCleanup !== null) { + if (windowCleanup.has(runtimeId)) { + windowCleanup.get(runtimeId).forEach(fn => { + try { fn(); } catch(e) { console.warn(e); } + }); + windowCleanup.delete(runtimeId); + } + } + + // DOM entfernen + win.remove() + } catch (err) { + alert(err.message) + } + +} + + +// function applyWindowState(win, payload) { +// const state = payload.state || 'normal'; + +// if (state === 'minimized') { +// win.style.display = 'none'; +// win.dataset.state = 'minimized'; +// } + +// if (state === 'maximized') { +// win.dataset.state = 'maximized'; +// win.classList.add('max'); + +// win.style.left = '8px'; +// win.style.top = '8px'; +// win.style.width = 'calc(100% - 16px)'; +// win.style.height = 'calc(100% - 60px)'; +// } + +// if (state === 'normal') { +// win.dataset.state = 'normal'; +// } +// } + + + function toggleMaximize() { + if (!win.classList.contains('max')) { + // 🔥 VOR dem Maximieren speichern + storeNormalGeometry(win); + +applyMaximized(win); + } else { + const prev = JSON.parse(win.dataset.normalGeometry || '{}'); + + win.style.left = prev.left || '50px'; + win.style.top = prev.top || '50px'; + win.style.width = prev.width || '800px'; + win.style.height = prev.height || '600px'; + + win.classList.remove('max'); + win.dataset.state = 'normal'; + + // 🔥 WICHTIG: Snap/Drag Reset + win.dataset.snapped = ''; + } + + requestAnimationFrame(() => saveOpenWindows()); + } + + + // 🧲 Snap-to-Side + Drag Handling + makeDraggableWithSnap(win); +} + +function makeDraggableWithSnap(win) { + if (win.style.display === 'none') return; + + const title = win.querySelector('.window-titlebar'); + let isDown = false; + let startX, startY, startLeft, startTop; + let snapped = false; + const snapThreshold = 30; // Pixel vom Rand zum Einrasten + + title.addEventListener('touchstart', (ev) => { + if (ev.target.closest('.minimize, .maximize, .close')) return; + isDown = true; + const t = ev.touches[0]; + startX = t.clientX; + startY = t.clientY; + startLeft = parseInt(win.style.left || 0); + startTop = parseInt(win.style.top || 0); + document.body.classList.add('dragging'); + }, { passive: true }); + + document.addEventListener('touchmove', (ev) => { + if (!isDown) return; + const t = ev.touches[0]; + + const dx = t.clientX - startX; + const dy = t.clientY - startY; + + win.style.left = startLeft + dx + 'px'; + win.style.top = startTop + dy + 'px'; + + // Snap + const screenW = window.innerWidth; + const screenH = window.innerHeight; + + if (t.clientX <= snapThreshold) { + applySnap(win, 'left'); + } else if (t.clientX >= screenW - snapThreshold) { + applySnap(win, 'right'); + } else if (t.clientY <= snapThreshold) { + applySnap(win, 'top'); + } + }, { passive: true }); + + document.addEventListener('touchend', () => { + isDown = false; + document.body.classList.remove('dragging'); + saveOpenWindows() + }); + + title.addEventListener('mousedown', (ev) => { + if (ev.target.closest('.minimize, .maximize, .close')) return; + + isDown = true; + startX = ev.clientX; + startY = ev.clientY; + + const wasMax = win.classList.contains('max'); + + let restored = false; + + const onMove = (moveEv) => { + if (!wasMax || restored) return; + + const dy = moveEv.clientY - startY; + + // 👉 erst wenn 12px NACH UNTEN gezogen + if (dy > 12) { + restored = true; + + // Größe wiederherstellen + const prev = JSON.parse(win.dataset.normalGeometry || '{}'); + const width = parseFloat(prev.width) || 800; + const height = parseFloat(prev.height) || 600; + + let left = moveEv.clientX - width / 2; + let top = moveEv.clientY - 10; + + left = Math.max(0, Math.min(left, window.innerWidth - width)); + top = Math.max(0, Math.min(top, window.innerHeight - height)); + + win.style.left = left + 'px'; + win.style.top = top + 'px'; + win.style.width = width + 'px'; + win.style.height = height + 'px'; + + win.classList.remove('max'); + win.dataset.state = 'normal'; + win.dataset.snapped = ''; + + // wichtig: neue drag basis setzen + startX = moveEv.clientX; + startY = moveEv.clientY; + + const rect = win.getBoundingClientRect(); + startLeft = rect.left; + startTop = rect.top; + + document.removeEventListener('mousemove', onMove); + } + }; + + const onUp = () => { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + isDown = false; + document.body.classList.remove('dragging'); + }; + + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + + // 👉 normal drag only if NOT maximized + if (!wasMax) { + const rect = win.getBoundingClientRect(); + startLeft = rect.left; + startTop = rect.top; + } + + document.body.classList.add('dragging'); +}); + + + document.addEventListener('mousemove', (ev) => { + if (!isDown) return; + + const dx = ev.clientX - startX; + const dy = ev.clientY - startY; + + win.style.left = startLeft + dx + 'px'; + win.style.top = startTop + dy + 'px'; + + // Snap to edges + const screenW = window.innerWidth; + const screenH = window.innerHeight; + + if (ev.clientX <= snapThreshold) { + // Snap left + applySnap(win, 'left'); + snapped = 'left'; + } else if (ev.clientX >= screenW - snapThreshold) { + // Snap right + applySnap(win, 'right'); + snapped = 'right'; + } else if (ev.clientY <= snapThreshold) { + // Snap top (maximize) + applySnap(win, 'top'); + snapped = 'top'; + } else if (snapped && ev.clientX > snapThreshold * 2 && ev.clientX < screenW - snapThreshold * 2) { + // Wegziehen vom Rand → Restore + restoreFromSnap(win); + snapped = false; + } + }); + + document.addEventListener('mouseup', () => { + isDown = false; + document.body.classList.remove('dragging'); + + saveOpenWindows() + }); + + function applySnap(win, side) { + if(win.dataset.type == 'view') return; + if (!win.dataset.prevstyle) { + win.dataset.prevstyle = JSON.stringify({ + left: win.style.left, + top: win.style.top, + width: `${win.getBoundingClientRect().width}px`, + height: `${win.getBoundingClientRect().height}px`, + }); + } + + win.classList.add('max'); + + if (side === 'left') { + win.style.left = '0'; + win.style.top = '8px'; + win.style.width = '50%'; + win.style.height = `calc(100% - 60px)`; + } else if (side === 'right') { + win.style.left = '50%'; + win.style.top = '8px'; + win.style.width = '50%'; + win.style.height = `calc(100% - 60px)`; + } else if (side === 'top') { + applyMaximized(win); + } + + win.dataset.state = side === 'top' ? 'maximized' : 'normal'; + saveOpenWindows(); // <-- HIER (SEHR wichtig!) + } + + function restoreFromSnap(win, pointerX, pointerY) { + // 1️⃣ Alte Normalgröße wiederherstellen + const prev = JSON.parse(win.dataset.normalGeometry || '{}'); + + const width = parseFloat(prev.width) || 800; + const height = parseFloat(prev.height) || 600; + + // 2️⃣ Maus in die Fenster-Mitte setzen + let left = pointerX - width / 2; + let top = pointerY - height / 2; + + // 3️⃣ Fenster darf nicht aus dem Bildschirm rutschen + const screenW = window.innerWidth; + const screenH = window.innerHeight; + left = Math.max(0, Math.min(left, screenW - width)); + top = Math.max(0, Math.min(top, screenH - height)); + + // 4️⃣ Fenster anwenden + win.style.left = left + 'px'; + win.style.top = top + 'px'; + win.style.width = width + 'px'; + win.style.height = height + 'px'; + + win.classList.remove('max'); + win.dataset.state = 'normal'; + win.dataset.snapped = ''; + } + + +} + + +function bringToFront(win) { + // Wenn Fenster minimiert → nix machen + if (win.style.display === 'none') return; + const tb = document.querySelector(`.taskbar-item[data-winid="${win.dataset.winid}"]`); + if(tb) { + tb.classList.add('focus'); + resetFocus(win.dataset.winid); + + saveOpenWindows(); // <-- HIER + } + const currentZ = parseInt(win.style.zIndex || 0); + // Nur nach vorne holen, wenn nicht bereits ganz oben + if (currentZ < topZ) { + topZ += 1; + win.style.zIndex = topZ; + } +} + +function resetFocus(exceptID) { + Array.from(document.querySelectorAll('.taskbar-item')).filter(btn => btn.dataset.winid != exceptID).forEach(btn => { + btn.classList.remove('focus') + }) +} + +function restoreWindow(win, payload, taskbarBtn = null) { + const state = payload.state || 'normal'; + + if (payload.location) { + win.style.left = payload.location.left; + win.style.top = payload.location.top; + } + if (payload.size) { + win.style.width = payload.size.width; + win.style.height = payload.size.height; + } + + // zIndex wiederherstellen + if (payload.zIndex) { + win.style.zIndex = payload.zIndex; + topZ = Math.max(topZ, payload.zIndex); // topZ anpassen, damit bringToFront funktioniert + } + + win.dataset.state = state; + + if (state === 'minimized') { + win.style.display = 'none'; + if (taskbarBtn) { + taskbarBtn.classList.add('minimized'); + taskbarBtn.classList.remove('focus'); + } + } else { + win.style.display = 'flex'; + } + + if (state === 'maximized') { + win.classList.add('max'); + } + return; +} + + +function saveOpenWindows() { + let previous = ((v)=>Array.isArray(v)?v:[])(JSON.parse(localStorage.getItem(LS_KEY('openWindows')) || '[]')); + const openWindows = []; + + document.querySelectorAll('[data-winid]').forEach(win => { + if (win.dataset.type !== 'app') return; + if (!win.dataset.appname || !win.dataset.appview) return; + + let state = win.dataset.state || 'normal'; + let geometry; + + if (win.classList.contains('max') && state === 'normal') { + state = 'maximized'; + } + + const prev = previous.length == 0 ? {} : previous.find(w => + w.name === win.dataset.appname && + w.view === win.dataset.appview + ); + + if (win.dataset.state === 'minimized' && prev?.location && prev?.size) { + // 🟡 Minimiert → alte gespeicherte Geometrie benutzen + geometry = { + left: prev.location.left, + top: prev.location.top, + width: prev.size.width, + height: prev.size.height + }; + } else { + // 🟢 Sichtbar → echte Größe messen + const rect = win.getBoundingClientRect(); + geometry = { + left: win.style.left, + top: win.style.top, + width: win.style.width, + height: win.style.height + }; + } + + // letzte gültige Geometrie merken + win.dataset.lastGeometry = JSON.stringify(geometry); + + + //alert(win.dataset.appname + ': ' + state) + const entry = { + name: win.dataset.appname, + view: win.dataset.appview, + viewLabel: win.dataset.viewLabel, + state: state, + zIndex: parseInt(win.style.zIndex || 0) + }; + + if (win.dataset.state !== 'minimized') { + entry.location = { left: geometry.left, top: geometry.top }; + entry.size = { width: geometry.width, height: geometry.height }; + } else if (prev?.location && prev?.size) { + // minimiert → alte geometry trotzdem behalten + entry.location = prev.location; + entry.size = prev.size; + } + // alert(previous.find(w => w.name === entry.name && w.view === entry.view)); + if(previous.find(w => w.name === entry.name && w.view === entry.view)) { + // 🔁 In previous ersetzen + previous[previous.findIndex(w => w.name === entry.name && w.view === entry.view)] = entry; + } else { + // ➕ Neu hinzufügen + previous.push(entry); + } + openWindows.push(entry); + + }); + localStorage.setItem(LS_KEY('openWindows'), JSON.stringify(openWindows)); +} + + + +window.addEventListener('DOMContentLoaded', () => { + let saved = JSON.parse(localStorage.getItem(LS_KEY('openWindows')) || '[]') + + if (saved.length > 0) { + saved.sort((a,b) => (Number(a.zIndex) || 0) - (Number(b.zIndex) || 0)); + for (const payload of saved) { + // mainSocket.emit('open_app', payload); + openApp(payload); + } + } + + if (savedFontFamily) { setFontFamily(savedFontFamily); } + else { setFontFamily('Arial'); } + + if (savedFontSize) { setFontSize(savedFontSize); } + else { setFontSize(18); } + + if (savedTheme) { loadServerStyles(savedTheme); } + else { loadServerStyles('dark'); } +}); + + +} catch( err ) { + alert(err) +} + \ No newline at end of file diff --git a/public/javascript/pluginAPI.js b/public/javascript/pluginAPI.js new file mode 100644 index 0000000..fd7a32a --- /dev/null +++ b/public/javascript/pluginAPI.js @@ -0,0 +1,92 @@ +const pluginAPI = { + async update(name, updates) { + if(Object.keys(updates)[0] && Object.values(updates)[0] == "") { + updates[Object.keys(updates)[0]] = [ ]; + } + return await this._request( + `/api/plugins/${name}/update`, + 'POST', + { updates } + ); + }, + + async activation(name, state) { + return await this._request( + `/api/plugins/activation`, + 'POST', + { name, state } + ); + }, + + async create(name) { + return await this._request( + `/api/plugins/${name}/create`, + 'POST' + ); + }, + + async rename(name, newName) { + return await this._request( + `/api/plugins/${name}/rename`, + 'POST', + { newName } + ); + }, + + + async delete(name) { + return await this._request( + `/api/plugins/${name}/delete`, + 'POST' + ); + }, + + // --- zentrale Request-Funktion --- + async _request(url, method, body) { + try { + const options = { method }; + + if (body) { + options.headers = { 'Content-Type': 'application/json' }; + options.body = JSON.stringify(body); + } + + const res = await fetch(url, options); + + if (!res.ok) { + const text = await res.text(); + writeEventLog(4, 'CLIENT', `Request fehlgeschlagen: ${text}`); + throw new Error(`HTTP ${res.status}: ${text}`); + } + return res.json(); + } catch (err) { + writeEventLog(4, 'CLIENT', err); + throw err; + } + } + }; + + + +class AttachOnBlurChange { + constructor(el, callback) { + this.el = el; + this.callback = callback; + this.initial = this.el.value; + + this.el.addEventListener('blur', () => { + this.handleBlur(); + }); + } + + handleBlur() { + if (this.el.value === this.initial) return; + + const oldValue = this.initial; + const newValue = this.el.value; + + this.initial = newValue; + + this.callback(newValue, oldValue, this.el); + } +} \ No newline at end of file diff --git a/public/javascript/requiredFields.js b/public/javascript/requiredFields.js new file mode 100644 index 0000000..f554e7c --- /dev/null +++ b/public/javascript/requiredFields.js @@ -0,0 +1,89 @@ +function createRequiredProgress({ container, progress, onChange }) { + let total = 0; + let filled = 0; + + const body = typeof container === 'string' + ? document.querySelector(container) + : container; + + const progressEl = typeof progress === 'string' + ? document.querySelector(progress) + : progress; + + if (!body || !progressEl) return; + + function isElementVisible(el) { + const style = window.getComputedStyle(el); + return el.offsetParent !== null && style.opacity !== '0'; + } + + function updateRequiredProgress() { + const requiredElements = Array.from( + body.querySelectorAll('input[required], select[required], textarea[required]') + ).filter(isElementVisible); + + total = requiredElements.length; + filled = requiredElements.filter(el => + el.classList.contains('is-required-filled') + ).length; + + progressEl.textContent = `${filled} / ${total} Pflichtfelder ausgefüllt`; + progressEl.classList.toggle('complete', filled === total); + progressEl.classList.toggle('incomplete', filled !== total); + } + + function updateRequiredState(el) { + const isEmpty = + (el instanceof HTMLSelectElement && !el.value) || + (el instanceof HTMLInputElement && el.type !== 'checkbox' && !el.value.trim()) || + (el instanceof HTMLTextAreaElement && !el.value.trim()); + + el.classList.toggle('is-required-empty', isEmpty); + el.classList.toggle('is-required-filled', !isEmpty); + + updateRequiredProgress(); + + if (typeof onChange === 'function') { + onChange({ + element: el, + isEmpty, + isFilled: !isEmpty, + isFinished: total === filled + }); + } + } + + function initialize() { + const elements = Array.from( + body.querySelectorAll('input[required], select[required], textarea[required]') + ).filter(isElementVisible); + + elements.forEach(el => { + updateRequiredState(el); + + el.addEventListener('input', () => updateRequiredState(el)); + el.addEventListener('change', () => updateRequiredState(el)); + }); + + updateRequiredProgress(); + } + + // 🔥 Reagiere auf Sichtbarkeitsänderungen + const observer = new MutationObserver(updateRequiredProgress); + observer.observe(body, { + attributes: true, + subtree: true, + attributeFilter: ['style', 'class', 'hidden'] + }); + + initialize(); + + // Optional: API zurückgeben + return { + initialize: initialize, + refresh: updateRequiredProgress, + destroy() { + observer.disconnect(); + } + }; +} diff --git a/public/javascript/tableFilter.js b/public/javascript/tableFilter.js new file mode 100644 index 0000000..8268054 --- /dev/null +++ b/public/javascript/tableFilter.js @@ -0,0 +1,450 @@ +/* + //Generic table filter module + // Usage: + TableFilter({ + table: document.querySelector('#myTable'), + exceptedColumns: [ 'Status_ID' ], + filterConfig: { + columnModes: { + ID: 'text', // Textsuche in dieser Spalte + Status: 'dropdown', // Dropdown-Werte aus Tabelle sammeln + Objekt: 'text', // Textsuche + Priorität: 'dropdown', + Gewerk: 'dropdown', + Typ: 'dropdown', + Bedarfsmelder: 'text' + Bearbeiter: 'text' + Genehmiger: 'text' + }, + checkboxFilter: { + column: 'Status_ID', + rules: [ + { label: 'Bearbeitung', test: v => [1,4,6,7,12,13,14].includes(parseInt(v)) }, + { label: 'Genehmigt', test: v => [2,5,9,10,11].includes(parseInt(v)) }, + { label: 'Abgelehnt', test: v => [3].includes(parseInt(v)) }, + { label: 'Abgeschlossen', test: v => [8].includes(parseInt(v)) } + ] + } + } + }); + // Generic table filter module (one textbox OR column-based dropdown) + // New features: + // - Selecting a column in the column-selector decides: textbox OR dropdown (configured in options) + // - If column-selector = empty value ⇒ full-row search + // - No extra checkbox needed + // - UI stays compact (only ONE input element + ONE dropdown) +*/ +function TableFilter(options) { + try { + + const table = options.table; + const cfg = options.filterConfig; + const exceptedColumns = options.exceptedColumns || []; + + // Gruppenerkennung + Verwaltung + const groupMap = new Map(); // groupRow → childRows[] + let groupsInitialized = false; + + const state = { + selectedColumn: '', + searchText: '', + dropdownValue: '', + dropdownCache: {}, + checkbox: new Set(), + }; + + //------------------------------------ + // Sticky Container für Filter + Header + //------------------------------------ + if(document.querySelectorAll('.table-filter-container').length > 0) { + // Array.from(document.querySelectorAll('.table-filter-container')).forEach(filterContainer => { + // filterContainer.remove(); + // }) + const existing = table.previousElementSibling; + if (existing && existing.classList.contains('table-filter-container')) { + existing.remove(); + } + } + + const container = document.createElement('div'); + container.className = 'table-filter-container'; + container.style.position = 'sticky'; + container.style.top = '0px'; + container.style.left = '0px'; + container.style.zIndex = 20; + + // Filter UI wird hier reingesetzt + table.before(container); + + // Tabelle selbst: header sticky + const thead = table.querySelector('thead'); + + + const headerCells = Array.from(table.querySelectorAll('thead th')); + const getColumnIndex = name => headerCells.findIndex(c => c.textContent.trim() === name); + + //------------------------------------ + // Dynamisches Filter-UI + //------------------------------------ + function createDynamicFilterUI() { + const wrap = document.createElement('div'); + wrap.style="display:flex;flex-direction:row;align-items:center;gap:10px" + + const colSelectLabel = document.createElement('label'); + const colSelect = document.createElement('select'); + const emptyOpt = document.createElement('option'); + emptyOpt.value = ''; + emptyOpt.textContent = '-- Alles --'; + colSelect.appendChild(emptyOpt); + colSelectLabel.innerText = 'Spalte'; + + headerCells.forEach(cell => { + if(!exceptedColumns.includes(cell.textContent.trim())) { + const name = cell.textContent.trim(); + const opt = document.createElement('option'); + opt.value = name; + opt.textContent = name; + colSelect.appendChild(opt); + } + }); + + const input = document.createElement('input'); + input.type = 'search'; + input.style.display = 'none'; + + const select = document.createElement('select'); + select.style.display = 'none'; + select.innerHTML = ''; + + function populateDropdownForColumn(colName) { + const colIndex = getColumnIndex(colName); + const rows = Array.from(table.querySelectorAll('tbody tr')).filter(r => !r.classList.contains('grouprow')); + const values = new Set(); + rows.forEach(r => { + const cell = r.children[colIndex]; + if (cell) values.add(cell.textContent.trim()); + }); + const sorted = new Set([...values].sort((a, b) => a.localeCompare(b))); + select.innerHTML = '' + [...sorted].map(v => ``).join(''); + } + + colSelect.addEventListener('change', () => { + state.selectedColumn = colSelect.value; + state.searchText = ''; + state.dropdownValue = ''; + input.value = ''; + select.value = ''; + + if (!state.selectedColumn) { + input.style.display = ''; + select.style.display = 'none'; + return applyFilters(); + } + + const mode = cfg.columnModes[state.selectedColumn]; + if (mode === 'text') { + input.style.display = ''; + select.style.display = 'none'; + } else if (mode === 'dropdown') { + input.style.display = 'none'; + select.style.display = ''; + populateDropdownForColumn(state.selectedColumn); + } else { + input.style.display = ''; + select.style.display = 'none'; + } + applyFilters(); + }); + input.style.display = ''; + + input.addEventListener('input', () => { state.searchText = input.value.toLowerCase(); applyFilters(); }); + select.addEventListener('change', () => { state.dropdownValue = select.value; applyFilters(); }); + + wrap.append(colSelectLabel, colSelect, document.createElement('br')); + wrap.append(input, select); + container.appendChild(wrap); + } + + //-------------------------------------------- +// Filterbreite an Parent minus Scrollbar +//-------------------------------------------- +function updateFilterWidth() { + if (!table || !container) return; + + const wrapper = table.parentElement; // z. B. .table-wrapper + if (!wrapper) return; + + const style = getComputedStyle(table.querySelector('thead th')); + + const rect = wrapper.getBoundingClientRect(); + const scrollbarWidth = wrapper.offsetWidth - wrapper.clientWidth; + + container.style.width = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px"; + container.style.maxWidth = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px"; +} + +// Initial setzen +updateFilterWidth(); + +// Fenstergröße beobachten +window.addEventListener('resize', updateFilterWidth); + +// Dynamische Anpassung bei Wrapper Resize +const wrapper = table.parentElement; +if (wrapper) { + new ResizeObserver(updateFilterWidth).observe(wrapper); +} + + + //------------------------------------ + // Checkbox Filter + //------------------------------------ + function createCheckboxFilter() { + const cfgCheck = cfg.checkboxFilter; + const idx = getColumnIndex(cfgCheck.column); + if (idx < 0) return; + + const wrapper = document.createElement('div'); + + cfgCheck.rules.forEach(rule => { + const checkWrapper = document.createElement('label'); + const checkInput = document.createElement('input'); + const checkTrack = document.createElement('span'); + const checkThumb = document.createElement('span'); + + checkWrapper.classList.add("cb", "cb-switch"); + checkWrapper.style.marginRight = '15px' + checkInput.type = 'checkbox'; + checkInput.addEventListener('change', () => { + if (checkInput.checked) state.checkbox.add(rule); + else state.checkbox.delete(rule); + applyFilters(); + }); + checkTrack.classList.add('switch-track'); + checkTrack.setAttribute("aria-hidden", 'true'); + + checkThumb.classList.add('switch-thumb'); + checkThumb.setAttribute("aria-hidden", 'true'); + + checkTrack.append(checkThumb) + checkWrapper.append(rule.label, checkInput, checkTrack); + wrapper.appendChild(checkWrapper); + }); + + container.appendChild(wrapper); + } + + //------------------------------------ + // Live Counter + //------------------------------------ + const counter = document.createElement('div'); + counter.className = 'live-counter'; + container.appendChild(counter); + + //------------------------------------ + // Filter Logik + //------------------------------------ + function detectGroups() { + if (groupsInitialized) return; + groupsInitialized = true; + + const rows = Array.from(table.querySelectorAll('tbody tr')); + let currentGroupRow = null; + let childBuffer = []; + + rows.forEach(row => { + if (row.classList.contains('grouprow')) { + // vorige Gruppe speichern + if (currentGroupRow && childBuffer.length > 0) { + groupMap.set(currentGroupRow, childBuffer); + } + // neue Gruppe starten + currentGroupRow = row; + childBuffer = []; + } + else if (currentGroupRow) { + childBuffer.push(row); + } + }); + + // letzte Gruppe speichern + if (currentGroupRow && childBuffer.length > 0) { + groupMap.set(currentGroupRow, childBuffer); + } + + // jedem groupRow ein Toggle verpassen (wenn nicht vorhanden) + groupMap.forEach((childRows, groupRow) => { + const toggle = groupRow.querySelector("span"); + + if (!toggle || toggle._groupToggleBound) return; + toggle._groupToggleBound = true; + + toggle.addEventListener("click", () => toggleGroup(groupRow)); + groupRow.addEventListener("dblclick", () => toggleGroup(groupRow)); + }); + } + + + function toggleGroup(groupRow) { + const childRows = groupMap.get(groupRow); + if (!childRows) return; + + const toggle = groupRow.querySelector("span"); + const collapsed = toggle.textContent === '+'; + + if (collapsed) { + // ausklappen → nur gefilterte Rows zeigen + childRows.forEach(r => { + if (!r._filteredOut) r.style.display = ''; + }); + toggle.textContent = '-'; + } else { + // einklappen → alle verstecken + childRows.forEach(r => r.style.display = 'none'); + toggle.textContent = '+'; + } + } + + + function applyFilters() { + detectGroups(); + const rows = Array.from(table.querySelectorAll('tbody tr')); + let visible = 0; + + rows.forEach(row => { + if (row.classList.contains('grouprow')) { row.style.display = ''; return; } + let show = true; + + if (state.selectedColumn === '') { + if (state.searchText) show = [...row.cells].some(c => c.textContent.toLowerCase().includes(state.searchText)); + } else { + const idx = getColumnIndex(state.selectedColumn); + const mode = cfg.columnModes[state.selectedColumn]; + if (mode === 'text' && state.searchText) { + show = row.cells[idx]?.textContent.toLowerCase().includes(state.searchText); + } + if (mode === 'dropdown' && state.dropdownValue) { + show = row.cells[idx]?.textContent === state.dropdownValue; + } + } + + if (state.checkbox.size > 0) { + const idx = getColumnIndex(cfg.checkboxFilter.column); + const cellVal = row.cells[idx]?.textContent ?? ''; + const pass = [...state.checkbox].some(rule => rule.test(cellVal)); + if (!pass) show = false; + } + + row._filteredOut = !show; // fürs Gruppensystem speichern + + // Wenn Gruppen existieren: + if (groupMap.size > 0) { + // Prüfen ob row zu einer Gruppe gehört + let parentGroup = null; + for (const [g, list] of groupMap.entries()) { + if (list.includes(row)) { + parentGroup = g; + break; + } + } + + if (parentGroup) { + const toggle = parentGroup.querySelector("span"); + const collapsed = toggle.textContent === '+'; + + if (collapsed) { + // eingeklappt → immer ausblenden + row.style.display = 'none'; + if (show) visible++; + } else { + // ausgeklappt → nur gefilterte anzeigen + row.style.display = show ? '' : 'none'; + if (show) visible++; + } + return; + } + + } else { + if (show) visible++; + } + + // normale Zeile ohne Gruppe: + row.style.display = show ? '' : 'none'; + }); + + counter.textContent = `${visible} Treffer`; + } + + //------------------------------------ + // Init + //------------------------------------ + createDynamicFilterUI(); + if(options.filterConfig.checkboxFilter) createCheckboxFilter(); + applyFilters(); + + + + if (thead) { + thead.style.position = 'sticky'; + thead.style.top = (container.offsetHeight - 1) + 'px'; + thead.style.zIndex = 10; + } + + //------------------------------------ + // Public API + //------------------------------------ + return { + refreshDropdown() { if (state.selectedColumn && cfg.columnModes[state.selectedColumn] === 'dropdown') {} }, + reapply() { applyFilters(); } + }; + + + } catch(err) { + alert(err.stack) + } +} + + + + + //#region Sort table + // let sortDirection = {}; + function sortTable(tableId,colIndex) { + const sortDirection = {}; + const table = document.getElementById(tableId); + const tbody = table.tBodies[0]; + const rows = Array.from(tbody.querySelectorAll("tr")); + + // Toggle sort direction + sortDirection[colIndex] = !sortDirection[colIndex]; + + rows.sort((a, b) => { + const cellA = a.cells[colIndex].textContent.trim(); + const cellB = b.cells[colIndex].textContent.trim(); + + // Numeric sort if possible + const numA = parseFloat(cellA); + const numB = parseFloat(cellB); + if (!isNaN(numA) && !isNaN(numB)) { + return sortDirection[colIndex] ? numA - numB : numB - numA; + } else { + return sortDirection[colIndex] + ? cellA.localeCompare(cellB) + : cellB.localeCompare(cellA); + } + }); + + // Remove old rows + tbody.innerHTML = ""; + rows.forEach(row => tbody.appendChild(row)); + + // Update sort arrows + const th = table.querySelectorAll("th"); + th.forEach((header, i) => { + header.classList.remove("sort-asc", "sort-desc"); + if (i === colIndex) { + header.classList.add(sortDirection[colIndex] ? "sort-asc" : "sort-desc"); + } + }); + } + //#endregion \ No newline at end of file diff --git a/public/javascript/tutorial.js b/public/javascript/tutorial.js new file mode 100644 index 0000000..5f59824 --- /dev/null +++ b/public/javascript/tutorial.js @@ -0,0 +1,354 @@ +class Tutorial { + constructor(steps, options = {}) { + this.steps = steps; + this.currentStep = 0; + this.isRunning = false; + this.isWaitingForEvent = false; + + this.options = { + overlayOpacity: 0.7, + ...options + }; + + this.zIndexBase = 1000; + this.zIndexStep = 0; + + this.modifiedElements = new Map(); + this.baseElement = null; + this.currentElement = null; + + this.tooltipObserver = null; + this.tooltipRAF = null; + this.currentStepElement = null; + } + + // ---------------- UI ---------------- + + createUI() { + document.addEventListener("keydown", e => { + if (this.isWaitingForEvent) return; + + if (e.key === "ArrowRight") this.next(); + if (e.key === "ArrowLeft") this.prev(); + }); + + this.tooltip = document.createElement("div"); + this.tooltip.id = "tutorial-tooltip"; + + Object.assign(this.tooltip.style, { + position: "absolute", + display: "flex", + padding: "12px", + borderRadius: "8px", + zIndex: 9999, + maxWidth: "350px", + boxShadow: "0 10px 30px rgba(0,0,0,0.2)", + backdropFilter: "blur(5px)", + justifyContent: "center" + }); + + this.text = document.createElement("p"); + + this.nextBtn = document.createElement("button"); + this.nextBtn.innerHTML = ">"; + this.nextBtn.classList.add("bluebutton"); + + this.prevBtn = document.createElement("button"); + this.prevBtn.innerHTML = "<"; + this.prevBtn.classList.add("yellowbutton"); + + this.nextBtn.onclick = () => this.next(); + this.prevBtn.onclick = () => this.prev(); + + this.tooltip.appendChild(this.text); + this.tooltip.appendChild(this.prevBtn); + this.tooltip.appendChild(this.nextBtn); + + document.body.appendChild(this.tooltip); + } + + // ---------------- Element wait ---------------- + + waitForElement(selector, timeout = 5000) { + return new Promise(resolve => { + const start = Date.now(); + + const check = () => document.querySelector(selector); + + let el = check(); + if (el) return resolve(el); + + const observer = new MutationObserver(() => { + el = check(); + if (el) { + observer.disconnect(); + resolve(el); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + const timer = setInterval(() => { + if (Date.now() - start > timeout) { + clearInterval(timer); + observer.disconnect(); + resolve(null); + } + + el = check(); + if (el) { + clearInterval(timer); + observer.disconnect(); + resolve(el); + } + }, 100); + }); + } + + // ---------------- Highlight ---------------- + + highlightElement(el) { + this.removeHighlight(); + + if (!this.modifiedElements.has(el)) { + this.modifiedElements.set(el, { + zIndex: el.style.zIndex, + position: el.style.position, + boxShadow: el.style.boxShadow + }); + } + + if (this.currentStep === 0 && !this.baseElement) { + this.baseElement = el; + } + + let zIndex; + + if (el === this.baseElement) { + zIndex = this.zIndexBase; + } else { + this.zIndexStep++; + zIndex = this.zIndexBase + this.zIndexStep; + } + + el.style.position = "relative"; + el.style.zIndex = zIndex; + el.style.boxShadow = + "0 0 0 4px rgba(255, 0, 0, 1), 0 0 20px rgba(255,0,0,0.7)"; + + this.currentElement = el; + + this.startTooltipTracking(el); + } + + removeHighlight() { + if (!this.currentElement) return; + + this.currentElement.style.boxShadow = ""; + this.stopTooltipTracking(); + } + + // ---------------- Event system (NEW) ---------------- + + getEventConfig(type) { + const map = { + click: () => new MouseEvent("click", { bubbles: true, cancelable: true, view: window }), + contextmenu: () => new MouseEvent("contextmenu", { bubbles: true, cancelable: true, view: window, button: 2 }), + dblclick: () => new MouseEvent("dblclick", { bubbles: true, cancelable: true, view: window }), + mouseenter: () => new MouseEvent("mouseenter", { bubbles: true, cancelable: true, view: window }), + mouseover: () => new MouseEvent("mouseover", { bubbles: true, cancelable: true, view: window }) + }; + + return map[type] ? map[type]() : null; + } + + async executeStepOnly(step, el) { + if (!step.action) return; + + const event = this.getEventConfig(step.action); + + if (event) { + el.dispatchEvent(event); + } + } + + waitForUserEvent(el, type) { + return new Promise(resolve => { + const handler = (e) => { + el.removeEventListener(type, handler); + resolve(e); + }; + + el.addEventListener(type, handler); + }); + } + + // ---------------- Tooltip tracking ---------------- + + startTooltipTracking(el) { + this.stopTooltipTracking(); + + this.currentStepElement = el; + + const update = () => { + if (!this.currentStepElement || !this.tooltip) return; + + const rect = this.currentStepElement.getBoundingClientRect(); + + this.tooltip.style.top = rect.bottom + 10 + "px"; + this.tooltip.style.left = rect.left + "px"; + + this.tooltipRAF = requestAnimationFrame(update); + }; + + this.tooltipObserver = new ResizeObserver(() => { + if (!this.currentStepElement) return; + + const rect = this.currentStepElement.getBoundingClientRect(); + + this.tooltip.style.top = rect.bottom + 10 + "px"; + this.tooltip.style.left = rect.left + "px"; + }); + + this.tooltipObserver.observe(el); + + this.tooltipRAF = requestAnimationFrame(update); + } + + stopTooltipTracking() { + if (this.tooltipObserver) { + this.tooltipObserver.disconnect(); + this.tooltipObserver = null; + } + + if (this.tooltipRAF) { + cancelAnimationFrame(this.tooltipRAF); + this.tooltipRAF = null; + } + + this.currentStepElement = null; + } + + // ---------------- Steps ---------------- + + async showStep() { + let step = this.steps[this.currentStep]; + this.updateButtons(step); + + + this.isWaitingForEvent = false; + + while (step && (!step.text || step.text.trim() === "")) { + const elSilent = await this.waitForElement(step.element); + + if (elSilent) { + await this.executeStepOnly(step, elSilent); + } + + this.currentStep++; + + if (this.currentStep >= this.steps.length) { + this.destroy(); + return; + } + + step = this.steps[this.currentStep]; + } + + const el = await this.waitForElement(step.element); + + if (!el) { + this.next(); + return; + } + + this.highlightElement(el); + + el.scrollIntoView({ behavior: "smooth", block: "center" }); + + const rect = el.getBoundingClientRect(); + + this.tooltip.style.display = "block"; + this.text.innerHTML = step.text; + + this.tooltip.style.top = rect.bottom + 10 + "px"; + this.tooltip.style.left = rect.left + "px"; + + this.isWaitingForEvent = !!step.waitFor; + + this.nextBtn.style.display = step.waitFor ? "none" : "inline-block"; + this.prevBtn.style.display = + this.currentStep === 0 || step.waitFor ? "none" : "inline-block"; + + if (step.waitFor) { + await this.waitForUserEvent(el, step.waitFor); + this.isWaitingForEvent = false; + this.next(); + } + } + + updateButtons(step) { + const isLastStep = this.currentStep === this.steps.length - 1; + + this.nextBtn.innerHTML = isLastStep ? "Fertig" : ">"; + } + + + // ---------------- Navigation ---------------- + + start() { + if (this.isRunning) return; + + this.isRunning = true; + this.createUI(); + this.currentStep = 0; + this.showStep(); + } + + next() { + if (this.isWaitingForEvent) return; + + this.removeHighlight(); + + if (this.currentStep < this.steps.length - 1) { + this.currentStep++; + this.showStep(); + } else { + this.destroy(); + } + } + + prev() { + if (this.isWaitingForEvent) return; + + this.removeHighlight(); + + if (this.currentStep > 0) { + this.currentStep--; + this.showStep(); + } + } + + // ---------------- Cleanup ---------------- + + destroy() { + this.isRunning = false; + + this.stopTooltipTracking(); + + this.modifiedElements.forEach((styles, el) => { + el.style.zIndex = styles.zIndex || ""; + el.style.position = styles.position || ""; + el.style.boxShadow = styles.boxShadow || ""; + }); + + this.modifiedElements.clear(); + this.baseElement = null; + this.zIndexStep = 0; + + this.tooltip?.remove(); + } +} \ No newline at end of file diff --git a/public/styles/colors.css b/public/styles/colors.css new file mode 100644 index 0000000..a8f18fe --- /dev/null +++ b/public/styles/colors.css @@ -0,0 +1,131 @@ +/* #region OS */ +#desktop { background:var(--theme-desktop-backcolor); background-image:var(--theme-desktop-image); } +#taskbar { background:var(--theme-taskbar-backcolor); color:var(--theme-taskbar-color); } +#tutorial-tooltip {background:var(--theme-window-backcolor); color:var(--theme-window-color);} +#tutorial-tooltip img {filter: invert(1);} + +#start-btn { background:var(--theme-accent-default-backcolor); color:var(--theme-accent-default-color); } +#start-btn:hover { background:var(--theme-accent-hover-backcolor); color:var(--theme-accent-hover-color); } +#start-btn:active { background:var(--theme-accent-active-backcolor); color:var(--theme-accent-active-color); } + +/* #start-menu, .submenu { background:var(--theme-taskbar-backcolor); color:var(--theme-taskbar-color); } */ +#start-menu { padding-bottom:10px; background:var(--theme-startmenu-backcolor); color:var(--theme-startmenu-color); border:3px solid rgb(128,128,128); } +.start-submenu-head { background:var(--theme-startmenu-submenu-header-backcolor); color:var(--theme-startmenu-submenu-header-backcolor) } +.start-icon { background:var(--theme-accent-default-backcolor); } + +/* .start-item:hover, .start-sys-item:hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); } */ +.start-item.has-submenu.open > .menu-label , .start-item:not(.has-submenu.open), .start-sys-item { background-color:var(--theme-startmenu-item-default-backcolor); color:var(--theme-startmenu-item-default-color); } +.start-item:not(.has-submenu.open):not(.unload):hover, .start-sys-item:hover { background-color:var(--theme-startmenu-item-hover-backcolor); color:var(--theme-startmenu-item-hover-color); } +.start-item:not(.has-submenu):not(.has-submenu.open):not(.unload):active, .start-sys-item:active { color:var(--theme-accent-active-color); background:var(--theme-accent-active-backcolor); } +.start-item-sys-container { background:var(--theme-startmenu-syscontainer-backcolor);} +.start-item .unload { background:var(--theme-startmenu-item-disabled-backcolor); color:var(--theme-startmenu-item-disabled-color); } +/* #taskbar .taskbar-item { background:var(--theme-taskbar-item-backcolor); } */ + +.taskbar-item { position: relative;} +.taskbar-item::before { background: var(--theme-accent-active-color); } +.taskbar-item.focus::before { background: var(--theme-accent-active-backcolor); } +/* .taskbar-item.minimized { background:var(--theme-taskbar-item-minimized-backcolor); color:var(--theme-taskbar-item-minimized-color); border-color:var(--theme-taskbar-item-minimized-border-color);} */ +.taskbar-item.default { background:var(--theme-taskbar-item-default-backcolor); color:var(--theme-taskbar-item-default-color); border-color:var(--theme-taskbar-item-default-border-color);} +.taskbar-item:hover { background-color:var(--theme-startmenu-item-hover-backcolor); color:var(--theme-startmenu-item-hover-color); } + +.window { border:2px solid var(--theme-window-titlebar-backcolor); background:var(--theme-window-backcolor); } +.window-content { color:var(--theme-window-color); background:var(--theme-window-backcolor); } +.window-titlebar { color:var(--theme-window-titlebar-color); background:var(--theme-window-titlebar-backcolor); } +.window .controls button { color:var(--theme-window-titlebar-color); background:transparent; } +.window .controls button:hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); } +/* #endregion */ + + +/* #region Table */ +table, tr, td, th { border-color:var(--theme-table-border-color); } +table thead th { background-color:var(--theme-table-header-backcolor); color:var(--theme-table-header-color); border-color:var(--theme-table-border-color); } +table tbody tr:nth-child(even) { background-color:var(--theme-table-rows-even-backcolor); color:var(--theme-table-rows-even-color); } +table tbody tr:nth-child(odd) { background-color:var(--theme-table-rows-odd-backcolor); color:var(--theme-table-rows-odd-color); } +table tbody tr:not(.grouprow):not(.no-hover):hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); } +table tbody tr.grouprow { background-color:var(--theme-table-rows-grouprow-backcolor); color:var(--theme-table-rows-grouprow-color); } +table tbody tr.active, table tbody td.active { color:var(--theme-accent-active-color); background:var(--theme-accent-active-backcolor); } +.table-filter-container { border-color:var(--theme-table-border-color); background-color:var(--theme-table-header-backcolor); color:var(--theme-table-header-color); } +/* #endregion */ + + +/* #region Scrollbar */ +::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background:var(--theme-scrollbar-backcolor); } +::-webkit-scrollbar-thumb { background-color:var(--theme-scrollbar-thumb-default-backcolor); border-color:var(--theme-scrollbar-thumb-default-border-color); } +::-webkit-scrollbar-thumb:hover { background-color:var(--theme-scrollbar-thumb-hover-backcolor); border-color:var(--theme-scrollbar-thumb-hover-border-color); } +/* #endregion */ + + +/* #region Tooltip */ +.global-tooltip { background:var(--theme-tooltip-backcolor); color:var(--theme-tooltip-color); border-color:var(--theme-tooltip-border-color) } +/* #endregion */ + +/* #region feedbox */ +.feedbox { background:var(--theme-feedbox-backcolor); color:var(--theme-feedbox-color);box-shadow:0 5px 25px rgba(0,0,0,0.3); } +.feedbox-overlay { background:rgba(0,0,0,0.5); } +/* #endregion */ + + +/* #region Messagbox */ +.test, .test .countdown { color:var(--theme-message-test-color); background-color:var(--theme-message-test-backcolor); border-color:var(--theme-message-test-border-color); } +.success, .success .countdown { color:var(--theme-message-success-color); background-color:var(--theme-message-success-backcolor); border-color:var(--theme-message-success-border-color); } +.error, .error .countdown { color:var(--theme-message-error-color); background:var(--theme-message-error-backcolor); border-color:var(--theme-message-error-border-color); } +.info, .info .countdown { color:var(--theme-message-info-color); background-color:var(--theme-message-info-backcolor); border-color:var(--theme-message-info-border-color); } +.warn, .warn .countdown { color:var(--theme-message-warn-color); background-color:var(--theme-message-warn-backcolor); border-color:var(--theme-message-warn-border-color); } +.throw_exception, .throw_exception .countdown { color:var(--theme-message-throw-color); background-color:var(--theme-message-throw-backcolor); border-color:var(--theme-message-throw-border-color); } +.message { box-shadow:0 4px 10px rgba(0,0,0,0.3); } /* SHADOW? */ +/* #endregion */ + +.error-field { background:var(--theme-message-error-backcolor); border-color:var(--theme-message-error-border-color); color:var(--theme-message-error-color); } +.success-field { background:var(--theme-message-success-backcolor); border-color: var(--theme-message-success-border-color); color:var(--theme-message-success-color); } + +/* region Container*/ +.card { background:var(--theme-container-card-backcolor); border-color:var(--theme-container-card-border); } +/* #endregion */ + + +/* #region Checkbox */ +.cb-box { border-color:var(--theme-checkbox-default-border-color); box-shadow:0 1px 2px var(--theme-checkbox-shadow-color) inset; } +.cb input:checked + .cb-box { background:var(--theme-checkbox-default-thumb); border-color:var(--theme-checkbox-default-thumb); } +.cb input:disabled + .cb-box { background:var(--theme-checkbox-disabled-backcolor); border-color:var(--theme-checkbox-disabled-border-color); } +.cb-label { color:var(--theme-checkbox-color); } +/* #endregion */ + +/* #region Switch */ +.cb-switch .cb-label { color:var(--theme-switch-color); } +.switch-track { background:var(--theme-switch-backcolor); border-color:var(--theme-switch-border-color); } +/* #endregion */ + +/* #region Required */ +.is-required-empty { border-width:2px; border-color:var(--theme-required-empty); border-style:solid; } +.is-required-filled { border-color:var(--theme-required-accept); } +/* #endregion */ + + +/* #region Tabs */ +.tabs { border-bottom-color: #ccc; } +.tab-content { border-color: #ccc; } +.tab:hover { background-color: var(--theme-accent-hover-backcolor); color: var(--theme-accent-hover-color); } +.tab.active { background-color: var(--theme-accent-active-backcolor); color: var(--theme-accent-active-color); border-color: var(--theme-accent-active-border-color); border-bottom-color: var(--theme-accent-active-backcolor); } +/* #endregion */ + +/* #region DOM */ +button:not(:disabled).monolyth { background-color:var(--theme-button-monolyth-default-backcolor); color:var(--theme-button-monolyth-default-color); } +button:not(:disabled).monolyth:hover { background-color:var(--theme-button-monolyth-hover-backcolor); color:var(--theme-button-monolyth-hover-color); } +button:not(:disabled).monolyth:hover, button:not(:disabled).bluebutton:hover, button:not(:disabled).redbutton:hover, button:not(:disabled).yellowbutton:hover, button:not(:disabled).greenbutton:hover { box-shadow:0 6px 8px rgba(0,0,0,0.15); } + +select { background:var(--theme-select-default-backcolor); color:var(--theme-select-default-color); box-shadow:0 0 5px var(--theme-select-default-border-color); } +input:hover, input[type="text"]:hover, input[type="email"]:hover, input[type="password"]:hover, input[type="number"]:hover, input[type="date"]:hover,textarea:hover, select:hover { background:var(--theme-input-hover-backcolor); color:var(--theme-input-hover-color); box-shadow:0 0 5px var(--theme-input-hover-border-color); } +input:focus, input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus, input[type="number"]:focus, input[type="date"]:focus, textarea:focus, select:focus { background:var(--theme-input-focus-backcolor); color:var(--theme-input-focus-color); box-shadow:0 0 5px var(--theme-input-focus-border-color); } +input.valid { border: 2px solid var(--theme-input-accept-border-color); } +input.invalid { border: 2px solid var(--theme-input-decline-border-color); } + +*::placeholder, input[id="sAMAccountName"] { color:var(--theme-input-placeholder-color); } + +textarea, input { border-color:var(--theme-input-default-border-color); background:var(--theme-input-default-backcolor); color:var(--theme-input-default-color); } +textarea:not([required]):hover, input:not([required]):hover { border-color:var(--theme-input-hover-border-color); background-color:var(--theme-input-hover-backcolor); color:var(--theme-input-hover-color); } +textarea:focus, input:focus { border-color:var(--theme-input-focus-border-color); background-color:var(--theme-input-focus-backcolor); color:var(--theme-input-focus-color); } + +select, select option { border-color:var(--theme-select-border-color); background-color:var(--theme-select-backcolor); color:var(--theme-select-color); } +select:focus, select:not([required]):hover, select:focus option,select:not([required]):hover option { border-color:var(--theme-select-hover-border-color); background-color:var(--theme-select-hover-backcolor); color:var(--theme-select-hover-color); } + +/* #endregion */ \ No newline at end of file diff --git a/public/styles/contextMenu.css b/public/styles/contextMenu.css new file mode 100644 index 0000000..fb079fe --- /dev/null +++ b/public/styles/contextMenu.css @@ -0,0 +1,79 @@ +.ctx-menu { + position: fixed; + min-width: 180px; + background: var(--theme-contextmenu-backcolor); + border: 1px solid var(--theme-contextmenu-border-color); + color: var(--theme-contextmenu-color); + border-radius: 6px; + padding: 4px 0; + box-shadow: 0 8px 20px rgba(0,0,0,0.15); + z-index: 2; +} + +.ctx-menu.hidden { + opacity: 0; + display: none; +} + +.ctx-list { + list-style: none; + margin: 0; + padding: 0; +} + +.ctx-item { + padding: 5px 7px 5px 20px; + margin-left: 1px; + white-space: nowrap; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; +} + +.ctx-item:hover { + background: var(--theme-contextmenu-hover-backcolor); + color: var(--theme-contextmenu-hover-color); +} + + +.ctx-item .icon { + display: inline-flex; + position: relative; + justify-content: flex-start; + width: 20px; + } + + +.ctx-disabled { + opacity: 0.5; + /* cursor: default; */ +} + +.ctx-divider { + border-top: 1px solid var(--theme-contextmenu-divider); + margin: 4px 0; +} + +.ctx-submenu { + position: absolute; + left: 100%; + top: 0; + min-width: 160px; + background: var(--theme-contextmenu-backcolor); + border: 1px solid var(--theme-contextmenu-border-color); + color: var(--theme-contextmenu-color); + padding: 4px 0; + border-radius: 6px; + display: none; + z-index: 99999; +} + +.ctx-submenu.open { + display: block; +} + +.ctx-arrow { + font-size: 11px; + opacity: 0.6; +} diff --git a/public/styles/default.css b/public/styles/default.css new file mode 100644 index 0000000..408e9ac --- /dev/null +++ b/public/styles/default.css @@ -0,0 +1,434 @@ +/* #region DOM */ +*::placeholder { text-align:center; } +* { + user-select:none; + cursor: var(--theme-cursor-default) 1 1, auto; +} + + +.selectable { user-select: text !important; cursor: var(--theme-cursor-pointer) 0 16, pointer; } + +input, input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="date"], textarea, select { border-width:2px; border-style:solid; font-size: var(--fontSize); font-family: var(--fontFamily); border-radius:10px; padding:10px 12px; outline:none; transition:all .5s ease; width:auto; } +input.width\:100px { width:100px; } +input.width\:90px { width:90px; } +input.width\:75px { width:75px; } +input.width\:50px { width:50px; } +input.width\:25px { width:25px; } + +*::placeholder, input[id="sAMAccountName"] { font-style:italic; font-weight:100; letter-spacing:3px; } + +html, button { font-size: var(--fontSize); font-family: var(--fontFamily); } +button.monolyth, button.bluebutton, button.greenbutton, button.yellowbutton, button.redbutton { display:inline-block; padding:8px 10px; margin:0.2rem 1.6rem; font-weight:600; text-align:center; text-decoration:none; color:rgb(255, 255, 255); border:none; border-radius:8px; box-shadow:0 4px 6px rgba(0,0,0,0.1); transition:all 0.2s ease; } +button.monolyth { background-color:transparent; } +button:not(:disabled).monolyth:hover { opacity:0.9; } +button.bluebutton { color:var(--theme-button-blue-default-color); background:var(--theme-button-blue-default-backcolor); } +button.greenbutton { color:var(--theme-button-green-default-color); background:var(--theme-button-green-default-backcolor); } +button.yellowbutton { color:var(--theme-button-yellow-default-color); background:var(--theme-button-yellow-default-backcolor); } +button.redbutton { color:var(--theme-button-red-default-color); background:var(--theme-button-red-default-backcolor); } +button:disabled { color:var(--theme-button-disabled-color); background:var(--theme-button-disabled-backcolor); } +button:not(:disabled).redbutton:hover { background:var(--theme-button-red-hover-color); background:var(--theme-button-red-hover-backcolor); } +button:not(:disabled).greenbutton:hover { background:var(--theme-button-green-hover-color); background:var(--theme-button-green-hover-backcolor); } +button:not(:disabled).bluebutton:hover { background:var(--theme-button-blue-hover-color); background:var(--theme-button-blue-hover-backcolor); } +button:not(:disabled).yellowbutton:hover { background:var(--theme-button-yellow-hover-color); background:var(--theme-button-yellow-hover-backcolor); } +/* #endregion */ + + + +/* #region Container */ +.container.static { width:calc(100% - 20px); margin:10px auto; display:flex; gap:12px; min-height:0; overflow:auto; max-height:100%; flex-direction: column;} +/* .card.static { display:flex; flex-direction:column;flex: 0 0 auto; } */ +.card.static.row { overflow:hidden; display:flex; flex-direction:row; flex-wrap: wrap;} +.card.static { overflow:hidden; display:flex; flex-direction:column; } + +.container { width:calc(100% - 20px); margin:10px auto; display:grid; grid-template-columns:100%; gap:12px; min-height:0; overflow:auto; max-height:100%; } +.container:not(.static) * { box-sizing:border-box; } +.card { border-width:1px; border-style:solid; border-radius:8px; padding:20px; } + +.grid { display:grid; gap:16px; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); } +/* #endregion */ + + +/* #region Copy icon */ +.copy-icon { transition:opacity .25s; cursor:var(--theme-cursor-pointer) -16 16, pointer; opacity:0.7; } +.copy-icon:hover { opacity:1; } +/* #endregion */ + + +/* #region Tooltip */ +.global-tooltip { position:fixed; padding:6px 8px; border-width:1px; border-style:solid; border-radius:10px; max-width:50vw; white-space:normal; z-index:999; pointer-events:none; opacity:0; transform:translateY(-30px); transition:opacity .12s ease; } +.global-tooltip.visible { opacity:1; } +/* #endregion*/ + + +/* #region Scrollbar */ +::-webkit-scrollbar { width:12px; height:12px; } +::-webkit-scrollbar-track { border-radius:0px; } +::-webkit-scrollbar-thumb { border-radius:6px; border-width:2px; border-style:solid; } +/* #endregion */ + + +/* #region Messagebox */ +#message-container { position: fixed; top: 1rem; right: 1px; display: flex; flex-direction: column; gap: 0.5rem; z-index: 1000; max-height: 80vh; padding-left: 15px; overflow-y: auto; overflow-x: visible; scrollbar-width: none; -ms-overflow-style: none; } +#message-container::-webkit-scrollbar { display: none; } +.message { border-radius: 8px; margin: 8px 8px 8px 0; padding: 10px 14px; width: auto; max-width: 45vw; animation: slideIn 0.4s ease-out; transition: transform 0.2s ease; word-break: break-word; overflow-wrap: anywhere; } +.message:hover { transform:scale(1.02); } +.message-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; } +.message-title { flex:1; font-weight:600; } +.countdown { margin-right:8px; white-space:nowrap; } +.pin-div { margin-left:8px; cursor:var(--theme-cursor-pointer) -16 16, pointer; user-select:none; transition:transform 0.2s ease, color 0.2s ease; } +.pin-div:hover { transform:scale(1.2); } +.pin-div.pinned { transform:scale(1.1); } +@keyframes slideIn { 0% { opacity:0; transform:translateX(100%); } 60% { opacity:1; transform:translateX(-10px); } 80% { opacity:1; transform:translateX(5px); } 100% { opacity:1; transform:translateX(-10px); } } +@keyframes slideOut { 0% { opacity:1; transform:translateX(0); } 100% { opacity:1; transform:translateX(100%); } } +/* #endregion */ + + +/* #region Toggle switch */ + /* + + */ +.cb-switch { --w:45px; --h:27px; display:inline-flex; align-items:center; } +.cb-switch input { position:absolute; opacity:0; width:0; height:0; pointer-events:none; } +.switch-track { width:var(--w); height:var(--h); border-radius:999px; padding:3px; border-width:1px; border-style:solid; box-sizing:border-box; display:inline-flex; align-items:center; transition:background .18s ease, transform .12s ease, border-color .25s ease; } +.switch-thumb { min-width:calc(var(--h) - 2 * 3px); width:calc(var(--h) - 2 * 3px); height:calc(var(--h) - 2 * 3px); background:var(--theme-switch-thumb); border-radius:50%; box-shadow:0 2px 6px rgba(0,0,0,.15); transform:translateX(-1px); transition:transform .25s cubic-bezier(.2,.9,.2,1), background .18s ease } + + + +.cb-switch input:disabled + .switch-track { background-color: dimgray;} +.cb-switch input:disabled + .switch-track .switch-thumb { background-color: rgb(54, 50, 50); } + +.cb-switch input:not(:disabled):checked + .switch-track { background:var(--theme-switch-active); } + +.cb-switch input:not(:disabled):hover + .switch-track { border-color:var(--theme-switch-hover); } + +.cb-switch input:focus-visible + .switch-track { box-shadow:0 0 0 6px rgba(6,193,103,0.12); } +.cb-switch input:checked + .switch-track .switch-thumb { transform:translateX(calc(var(--w) - var(--h))); } +.cb-switch label { width: calc(100% - var(--w)); } +/* #endregion */ + + +/* #region CheckBox */ + /* + + */ +.cb { display:inline-flex; align-items:center; gap:10px; user-select:none; transform:translateY(2px); } +.cb input { position:absolute; opacity:0; width:0; height:0; pointer-events:none; } +.cb-box { width:20px; height:20px; border-radius:6px; background:var(--theme-checkbox-backcolor); border-width:2px; border-style:solid; display:inline-grid; place-items:center; transition:transform .12s ease, border-color .12s ease, background .12s ease; } +.cb-box::after { content:""; width:12px; height:8px; transform:scale(0) translateY(-2px); background-repeat:no-repeat; background-position:center; background-size:contain; background-image:url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 4L4 7l7-7' stroke='%23fff' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); transition:transform .18s cubic-bezier(.2,.9,.2,1); } +.cb input:checked + .cb-box { transform:translateY(-1px); } +.cb input:checked + .cb-box::after { transform:scale(1) translate(-1px, 1px); } +table .cb input:checked + .cb-box::after { transform:scale(1) translate(1px, 1px); } +.cb input:focus-visible + .cb-box { outline:none; } +/* #endregion */ + + +/* #region Required */ +.is-required-empty { border-width:2px; border-style:solid; } +/* #endregion */ + + +/* #region Tabs */ +.tabs { display: flex; margin-bottom: 10px; border-bottom-width: 2px; border-bottom-style: solid; } +.tab { padding: 10px 20px; border: 1px solid transparent; border-top-left-radius: 5px; border-top-right-radius: 5px; margin-right: 5px; transition: background .25s, color .25s, border-color .25s; } +.tab-content { border-width: 1px; border-style: solid; border-radius: 5px; padding: 15px; } +.item { margin-bottom: 5px; } +/* #endregion */ + + +/* #region Feebox */ +.feedbox-overlay { position:fixed; top:0; left:0; width:100vw; height:100vh; display:flex; align-items:center; justify-content:center; z-index:999; } +.feedbox { border-radius:8px; max-width:50vw; width:100%; padding:20px; animation:feedboxFadeIn 0.2s ease-out; max-height:80vh; display:flex; flex-direction:column; overflow:hidden; } +.feedbox h3 { margin:0 0 10px 0; } +.feedbox-message { margin-bottom:20px; line-height:1.4; flex:1; overflow-y: auto; /* font-size:1rem; */ } +.feedbox-actions { display:flex; justify-content:flex-end; gap:10px; flex-wrap:wrap; flex-shrink:0; } +@keyframes feedboxFadeIn { from { transform:translateY(-20px); opacity:0; } to { transform:translateY(0); opacity:1; } } + +.feedbox-btn { + padding:6px 14px; + /* font-size:0.95rem; */ + border-radius:4px; + border:1px solid #ccc; + background:#f5f5f5; + transition:all 0.15s; +} + +.feedbox-btn:hover { + background:#e5e5e5; +} + +.feedbox-btn.primary { + background:#4a90e2; + color:#fff; + border-color:#4a90e2; +} + +.feedbox-btn.primary:hover { + background:#357ABD; +} + +.feedbox-btn.danger { + background:#e94e3c; + color:#fff; + border-color:#e94e3c; +} + +.feedbox-btn.danger:hover { + background:#c0392b; +} + +.feedbox-input { + width:100%; + padding:6px 8px; + /* font-size:0.95rem; */ + margin-top:8px; + margin-bottom:12px; + border:1px solid #ccc; + border-radius:4px; + box-sizing:border-box; +} + +.feedbox-btn:focus, .feedbox-input:focus { + outline:2px solid #4a90e2; + outline-offset:2px; +} + +/* Responsive Small Screens */ +@media (max-width:400px) { + .feedbox { + padding:15px; + } + + .feedbox-actions { + flex-direction:column-reverse; + gap:8px; + } + + .feedbox-btn { + width:100%; + } +} + +/* #endregion */ + + + + + + + +.select-wrapper { + position: relative; + width: 250px; +} + +.select-wrapper select { + width: 100%; + padding: 12px 40px 12px 14px; + font-size: 16px; + border: 2px solid #ddd; + border-radius: 10px; + background-color: #fff; + color: #333; + appearance: none; /* entfernt Standard-Styling */ + -webkit-appearance: none; + -moz-appearance: none; + cursor: pointer; + transition: all 0.2s ease; +} + +/* Hover / Focus */ +.select-wrapper select:hover { + border-color: #999; +} + +.select-wrapper select:focus { + outline: none; + border-color: #4a90e2; + box-shadow: 0 0 0 3px rgba(74,144,226,0.2); +} + +/* Custom Pfeil */ +.select-wrapper::after { + content: "▾"; + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + font-size: 14px; + color: #666; +} + + + +.error-field { padding:12px 16px; border-radius:10px; border-width:1px; border-style:solid; } +.success-field { border-width: 1px; border-style: solid; padding:12px 16px; border-radius:10px; } + +label { color:var(--muted); display:block; margin-bottom:6px; } + + +/* #region Dropzone */ +.dropzone { + max-width:360px; + padding:12px; + box-shadow:0 6px 18px rgba(0,0,0,.08); + + background:var(--theme-dropzone-default-backcolor); + + border:1px dashed #aaa; + border-radius:8px; + padding:10px; + /* font-size:13px; */ + text-align:center; + margin-bottom:8px; + color:var(--theme-dropzone-default-color); +} + +.dropzone.active { + border-color:var(--theme-dropzone-active-border-color); + color:var(--theme-dropzone-active-color); + background:var(--theme-dropzone-active-backcolor); +} + + +.dropzone-area ul { + list-style:none; + padding:0; + margin:0; + max-height:120px; + overflow-y:auto; +} + +.dropzone-area li { + display:flex; + justify-content:space-between; + align-items:center; + /* font-size:13px; */ + padding:4px 0; + border-bottom:1px solid #eee; +} + +button.removeButton { + border:none; + background:none; + color:#d11a2a; + cursor:var(--theme-cursor-pointer) -16 16, pointer; + /* font-size:14px; */ +} + + input[type="file"] { + display:none; + } + + tr.drop-hover { + outline:3px dashed var(--theme-dropzone-active-border-color); + /* outline:2px dashed var(--primary-color, #4a90e2); + background:rgba(74, 144, 226, 0.08); */ + } + + tr.no-drop-hover { + outline:3px dashed red; + cursor:no-drop !important; + /* outline:2px dashed var(--primary-color, #4a90e2); + background:rgba(74, 144, 226, 0.08); */ + } + +/* #endregion */ + + + + + + + + + +/* #region Multiselect textbox */ +.mst-wrapper { + position:relative; + display:block; + border-radius:4px; + padding:4px 8px; + background:var(--theme-window-backcolor); + + cursor:text; + width:300px; /* optional, je nach gewünschter Breite */ +} + +/* Eingabefeld oben */ +.mst-input { + width:100%; + border:none; + outline:none; + /* font-size:14px; */ + padding:4px 0; +} + +/* Container für Chips unterhalb des Inputs */ +.mst-chips-container { + display:flex; + flex-wrap:wrap; /* Chips umbrechen */ + gap:4px; + margin-top:4px; +} + +/* Einzelner Chip */ +.mst-chip { + display:inline-flex; + align-items:center; + background-color:#007bff; + color:#fff; + border-radius:8px; + padding:8px 8px; + /* font-size:13px; */ +} + +/* Entfernen-Button im Chip */ +.mst-chip-remove { + margin-left:4px; + cursor:var(--theme-cursor-pointer) -16 16, pointer; + font-weight:bold; +} + +/* Dropdown-Liste */ +.mst-dropdown { + position:absolute; + top:100%; + left:0; + right:0; + max-height:200px; + overflow-y:auto; + border:1px solid #ccc; + background:inherit; + z-index:1000; +} + +.mst-item { + padding:6px 8px; + cursor:var(--theme-cursor-pointer) -16 16, pointer; +} + +.mst-item:hover { + background-color:var(--theme-accent-hover-backcolor); +} + +.mst-item.new { + font-style:italic; + color:#555; +} + +/* #endregion */ + + +.tutorial-highlight { + transition: all 0.3s ease; +} \ No newline at end of file diff --git a/public/styles/jsonTree.css b/public/styles/jsonTree.css new file mode 100644 index 0000000..827ef5b --- /dev/null +++ b/public/styles/jsonTree.css @@ -0,0 +1,79 @@ +.json-controls { + position: sticky; + top: -8px; + backdrop-filter: blur(5px); + padding: 8px; + border-bottom: 2px solid var(--theme-window-titlebar-backcolor); + z-index: 1; +} + +.json-controls button { + background: var(--theme-window-backcolor) !important; + color: var(--theme-window-color) !important; +} + +.json-line { + padding: 3px 0; + white-space: pre; +} + +.json-key { + color: #d73a49; +} + +.json-input { + padding: 3px 8px; + border: none; + background: transparent; + outline: none; +} + +.json-input.string { + color: #032f62; +} + +.json-input.number { + color: #005cc5; +} + +.json-toggle { + cursor:var(--theme-cursor-pointer) -16 16, pointer; + color: #6f42c1; +} + +.json-children { + padding: 3px 0; +} + +.json-children.collapsed { + display: none; +} + +.json-header { + display: flex; + align-items: center; + gap: 6px; +} + +.json-add { + cursor:var(--theme-cursor-pointer) -16 16, pointer; + color: #28a745; + opacity: 0.7; + font-size: 13px; +} + +.json-add:hover { + opacity: 1; +} + +.json-remove { + cursor:var(--theme-cursor-pointer) -16 16, pointer; + color: #dc3545; + opacity: 0.6; + margin-left: 6px; + font-size: 12px; +} + +.json-remove:hover { + opacity: 1; +} \ No newline at end of file diff --git a/public/styles/os.css b/public/styles/os.css new file mode 100644 index 0000000..a31b004 --- /dev/null +++ b/public/styles/os.css @@ -0,0 +1,217 @@ + /* public/css/desktop.css */ +body, html { margin:0; padding:0; height:100%; overflow: hidden; font-family: var(--fontFamily); font-size: var(--fontSize); } +#desktop { position:relative; height:100vh; overflow:hidden; background-size:var(--theme-desktop-background-size); background-repeat: no-repeat; background-position: center; touch-action: none; } + +#windows { z-index: 1; position:absolute; inset:0; padding:8px; box-sizing:border-box; } +.window { position:absolute; width: 800px; height: 600px; border-radius:6px; box-shadow: 0 6px 20px rgba(0,0,0,0.4); overflow:hidden; top:50px; left:50px; display:flex; flex-direction:column; resize:both; } +.window-titlebar { padding: 0 0 1px 0; height: 28px; display:flex; justify-content:space-between; align-items:center; } +.window-icon { height: 20px; background-size:contain; transform: translate(1px, -1px); background-repeat: no-repeat; background-position: left; background-color: rgb(144, 179, 144); padding:4px;border-radius: 8px;} +.window-content { display: flex; flex-direction: column; flex:1; padding:8px; overflow: auto; } +.window .controls button { transition: background-color .3s, color .3s; padding: 6px 10px; border:none; } +.window[class="max"] .window-resize-handle { display: none; } +.window-resize-handle { position:absolute; right:0; bottom:0; width:12px; height:12px; cursor:se-resize; z-index: 10; } +.window-resize-n { top: -4px; left: 0; right: 0; height: 8px; cursor: n-resize; } +.window-resize-s { bottom: -4px; left: 0; right: 0; height: 8px; cursor: s-resize; } +.window-resize-e { right: -4px; top: 0; bottom: 0; width: 8px; cursor: e-resize; } +.window-resize-w { left: -4px; top: 0; bottom: 0; width: 8px; cursor: w-resize; } +.window-resize-ne { top: -4px; right: -4px; width: 12px; height: 12px; cursor: ne-resize; } +.window-resize-nw { top: -4px; left: -4px; width: 12px; height: 12px; cursor: nw-resize; } +.window-resize-se { bottom: -4px; right: -4px; width: 12px; height: 12px; cursor: se-resize; } +.window-resize-sw { bottom: -4px; left: -4px; width: 12px; height: 12px; cursor: sw-resize; } + + +#taskbar { z-index: 2; position: absolute; width:100%; bottom:0; left:0; height:42px; overflow:visible; display:flex; flex: 0 0 auto; min-width:0; align-items:center; padding:0 8px; box-sizing:border-box; } +#start-btn { transition: background-color 0.3s ease; padding: 8px 15px; border-radius: 5px; border: none; margin-right:8px; } +#taskbar-windows { display:flex; gap:6px; align-items:center; flex:1; overflow-y:hidden;overflow-x: auto; min-width: 0; } +.taskbar-item { display: flex; position: relative; padding:7px 10px; border-radius:4px; } +.taskbar-item::before { content: ''; position: absolute; bottom:1px; left:50%; width:40%; height: 4px; border-radius:4px; transform:translateX(-50%) scaleX(0); transform-origin:center; transition:transform 0.3s ease; } +.taskbar-item.focus::before { transform: translateX(-50%) scaleX(1); } + +.notify-button { margin-left:auto; flex: 0 0 auto; display: flex; align-items: center; justify-content: center; } +.notify-button.resume, .notify-button.pulse { animation: pulse 1.5s infinite; animation-play-state: running; } +.notify-button.pause, .notify-button.pause *, .notify-button.active, .notify-button.active * { animation-play-state: paused; } + + +@keyframes pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + } +} + + +.hidden { opacity: 0; pointer-events: none; } + +/* Open submenu vertical */ +#start-menu { z-index: 2; transition: opacity 0.3s; display:flex; flex-direction:column; position: absolute; bottom: 50px; left: 8px; width: auto; min-width: 300px; border-radius: 8px; overflow: hidden; } +.start-header { display: flex; align-items: center; padding: 8px; font-weight: 600; } +.start-header > img { height: 24px; width: 24px; margin-right: 5px; } +.start-list { list-style: none; margin: 0; padding: 8px 0; height: 60vh; overflow-y: auto; } +.start-item { padding: 8px 12px 16px 20px; display: flex; gap: 8px; transition: background 0.2s; border-radius: 4px; } +.start-item:not(:has(.submenu)) { padding: 12px;align-items:center; } +.start-icon, #start-menu-icon { height: 20px; width: 20px; border-radius: 8px; padding:4px; } +.menu-label { display: block; margin-top: 4px; padding-bottom: 0; margin-left: 26px } +.start-item.has-submenu { position: relative; display: flex; flex-direction: column; padding-bottom: 8px; } +.start-item.has-submenu > .submenu { width: 100%; list-style: none; padding-left: 2px; max-height: 0; overflow: hidden; transition: max-height 0.3s ease; margin: 2px 0 0 8px; } + +.start-item-sys-container { position: relative; left:0; bottom: -8px; padding: 5px 0px; width:100%; display:flex; height:30px; flex-direction:row; justify-content:flex-end; } +.start-sys-item { margin: 0 10px 0 8px !important; } + +.start-submenu-head { position: relative; margin-left: 16px; height:32px; display:flex; flex-direction: row; align-items: center; gap:8px; } +.start-item.has-submenu > .menu-label::after { content: ""; position: absolute; right: 14px; transform: translateY(50%) rotate(0deg); border: 5px solid transparent; border-top-color: #aaa; transition: transform 0.3s; } +.start-item.has-submenu.open > .menu-label::after { content: ""; position: absolute; right: 14px; transform: translateY(50%) rotate(180deg); border: 5px solid transparent; border-top-color: #aaa; transition: transform 0.3s; } +.start-item.has-submenu.open > .submenu { max-height: 500px; } +.start-item.has-submenu > .submenu li { opacity: 0; transform: translateY(-5px); transition: opacity 0.2s, transform 0.2s; } +.start-item.has-submenu.open > .submenu li { opacity: 1; transform: translateY(0); } + + +img.icon { width: auto; height:20px; object-fit: contain; filter: var(--theme-notifybubble-filter); transform: translate(2px, 2px) } + + + + +@media (max-width: 768px) { + + body, html { + overflow: auto; + } + + #desktop { + height: 100dvh; + } + + /* Fenster = Fullscreen */ + .window { + width: 100% !important; + height: calc(100% - 54px) !important; + top: 0 !important; + left: 0 !important; + border-radius: 0; + resize: none; + } + + .window-resize-handle { + display: none !important; + } + + .window-titlebar { + height: 29px; + padding: 0 0px; + } + + .window-content { + padding: 12px; + } + + /* Taskbar größer für Touch */ + #taskbar { + height: 50px; + padding: 0 6px; + } + + #start-btn { + padding: 10px 14px; + font-size: 16px; + } + + .taskbar-item { + padding: 10px; + } + +.start-list { list-style: none; margin: 0; padding: 8px 0; height: 100%; overflow-y: auto; } + +#start-menu { + bottom: 50px !important; + height: calc(100dvh - 65px) !important; + width: calc(100dvw - 5px); + left: 0 !important; + border-radius: 0 !important; + } */ + + .start-item { + padding: 14px; + font-size: 16px; + } + + .start-icon { + height: 24px; + width: 24px; + } + + .start-item.has-submenu > .menu-label { margin: 8px 0 0 40px; } + + .start-item-sys-container { margin:auto; bottom: -10px; } + + + + /* Buttons besser klickbar */ + .window .controls button { + padding: 10px 12px; + } + + /* Notify Button */ + .notify-button { + padding: 8px; + } + +} + +@media (max-width: 1000px) AND (orientation: landscape) { + + body, html { + overflow: auto; + } + + #desktop { + height: 100dvh; + } + /* Fenster IMMER fullscreen */ + .window { + width: 100% !important; + height: calc(100dvh - 46px) !important; + top: 0 !important; + left: 0 !important; + border-radius: 0 !important; + } + + /* Resize komplett aus */ + .window-resize-handle { + display: none !important; + } + + .window-titlebar { + height: 30px; + padding: 0 0px; + } + + + /* Startmenü bis ganz oben */ + #start-menu { + bottom: 44px !important; + height: calc(100dvh - 60px) !important; + width: 300px; + max-width: 35dvw; + left: 0 !important; + border-radius: 0 !important; + } + + /* Taskbar bleibt sichtbar, aber oben drüber sauber */ + #taskbar { + z-index: 9999; + } + + /* Startliste nutzt volle Höhe */ + .start-list { + height: 100%; + } + + .start-item-sys-container { bottom: -10px;} + + + .start-item.has-submenu > .menu-label { margin: 8px 0 0 28px; } +} \ No newline at end of file diff --git a/public/styles/table.css b/public/styles/table.css new file mode 100644 index 0000000..54090b4 --- /dev/null +++ b/public/styles/table.css @@ -0,0 +1,86 @@ +/* .table-wrapper { display:grid; grid-column:auto; overflow:auto; min-height:0; min-width:0;} */ +.table-wrapper { overflow:auto; min-width:0; } + +.table-wrapper.fit-table { grid-template-columns:calc(1fr - 150px); } +/* .table-wrapper { flex:1 1 auto; overflow-y:auto; } */ +/* .table-wrapper.fit-table { width:100%; max-width:100%; } */ +/* .table-wrapper.fit-table th.word-wrap, .table-wrapper.fit-table td.word-wrap { white-space:normal; word-wrap:break-word; word-break:break-word; overflow-wrap:anywhere; } */ + + +/* #region Dont's */ +th.no-wrap, td.no-wrap { white-space:nowrap !important; } +th.wrap, td.wrap { white-space:wrap; word-wrap: break-word; word-break: keep-all; } +table.no-background tr, table.no-background td { background:none !important; } + +table.border * { border:1px solid white; } +/* #endregion */ + + +/* #region performance optimizing */ +table thead { position:sticky; top:0; z-index:20; will-change:transform; transform:translateZ(0);} + + +/* #endregion */ +/* echte Tabelle */ +table { width:calc(100%); border-spacing:0 5px; } +table th, table td { min-width:100px; max-width:250px; overflow:hidden; white-space:nowrap; } + +table tr.grouprow:hover { background: rgba(0,0,0,0.05);} + +/* Header sticky — aber innerhalb echter Tabelle */ +thead th { position:sticky; top:var(--filter-height); } + +/* KEIN display:block mehr! */ +thead, tbody { display:table-row-group; } + + +table thead th { padding:5px; } +/* table tbody td { padding:5px 0px 5px 20px; } */ +table tbody td:not(:first-child):not(:last-child), table thead th:not(:first-child):not(:last-child) { border-width:0; border-style:solid; } +table tbody tr.grouprow { font-weight:700; } + +table .text-align\:center { text-align:center; } +table .text-align\:right { text-align:right; } +table .text-align\:left { text-align:left; } + +td { overflow:hidden; text-overflow:ellipsis; /* verhindert, dass Inhalt die Zelle sprengt */ } + +.table-filter-container { + border-bottom-width:8px; + border-bottom-style:solid; + display:flex; + justify-content:flex-start; + flex-direction:column; + flex-wrap:wrap; + gap:10px; + + position:sticky; + left:0px; + top:0px; + /* z-index:20; */ + padding:5px 10px; + border-radius:var(--border-raduis) var(--border-raduis) 0 0; +} +.table-filter-container .live-counter { position:absolute; right:18px; margin-left:auto; font-weight:bold; } +.table-filter-container input, .table-filter-container select { padding:5px !important; } + + + th.sort-asc::after { + content:" ▲"; + } + + th.sort-desc::after { + content:" ▼"; + } + + +td.vote { width:28px; height:28px; position:relative; padding:0; } + +td.vote:hover { background:rgba(0,0,0,0.05); /* cursor:pointer;*/ } +td.vote::before, +td.vote::after { position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); } +td.yes::before { content:""; width:8px; height:16px; border:solid #2ecc71; border-width:0 3px 3px 0; transform:translate(-50%, -60%) rotate(45deg); } +td.no::before, td.no::after { content:""; width:3px; height:18px; background:#e74c3c; } +td.no::before { transform:translate(-50%, -50%) rotate(45deg); } +td.no::after { transform:translate(-50%, -50%) rotate(-45deg); } +td.maybe::before { content:""; width:0px; height:0px; border-radius:50%; border:10px solid #f1c40f; transform:translate(-50%, -50%); } diff --git a/public/styles/userNotification.css b/public/styles/userNotification.css new file mode 100644 index 0000000..5438f81 --- /dev/null +++ b/public/styles/userNotification.css @@ -0,0 +1,67 @@ +/* Bubble-Container */ +#notify-bubble { + position: absolute; + bottom: 125%; + right: 3px; + min-width: 200px; + max-height: 50vh; + width: max-content; /* 🔑 wächst mit Inhalt */ + max-width: 600px; /* aber capped */ + background: var(--theme-taskbar-tray-backcolor); + color: var(--theme-taskbar-tray-color); + border-color: var(--theme-taskbar-tray-border-color); + border-radius: 10px; + overflow-y: auto; /* vertikal scrollen */ + overflow-x: hidden; /* horizontal verhindern */ + box-shadow: 0 8px 20px rgba(0,0,0,0.4); + padding: 10px 10px 6px 10px; + opacity: 0; + pointer-events: none; + transform: translateY(10px); + transition: all 0.25s ease; + z-index: 99999999; + text-align: end; + font-weight: normal; +} + +/* Bubble-Items als Grid */ +.bubble-item { + display: grid; + grid-template-columns: minmax(0, auto) 250px; + gap: 10px; + padding: 6px 8px; + border-radius: 6px; + /* cursor: pointer; */ + align-items: center; + transition: transform 0.25s; + +} + +/* linke Spalte */ +.bubble-item > :nth-child(1) { + min-width: 0; /* wichtig für Grid Shrink */ + white-space: normal; /* Text darf umbrechen */ + overflow-wrap: normal; /* Wörter nur bei Bedarf umbrechen */ + word-break: normal; /* nicht mitten in Wörtern */ + text-align: left; +} + +/* rechte Spalte fix */ +.bubble-item > :nth-child(2) { + width: 250px; + height: 50px; + overflow: hidden; +} + +/* hover Effekt */ +.bubble-item:hover { + background: rgba(64,64,64,0.4); + transform: scale(1.01); +} + +/* Bubble sichtbar machen */ +.notify-button.active #notify-bubble { + pointer-events: auto; + opacity:1; + transform: translateY(0); +} \ No newline at end of file diff --git a/public/views/desktop.hbs b/public/views/desktop.hbs new file mode 100644 index 0000000..df00f06 --- /dev/null +++ b/public/views/desktop.hbs @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + +
+
+ Radix OS + + +
+
+ + + + + +
+ +
+ +
+
+ + + + + + + + + + diff --git a/public/views/eventlog.hbs b/public/views/eventlog.hbs new file mode 100644 index 0000000..deffb0c --- /dev/null +++ b/public/views/eventlog.hbs @@ -0,0 +1,127 @@ + + + + + Event Log + + +
+
+
+ + + + + + + + + + + + + + {{#each logs}} + + + + + + + + + + {{/each}} + +
IDDatumLevelPluginMessageTraceUser
{{this.ID}}{{dateFormat this.Date "yyyy-mm-dd HH:MM:SS"}} + {{LevelDisplayName}} + {{this.PluginName}}{{replaceAll this.Message "||" "
"}}
"}}`)">⧉
{{this.Trace}}{{this.ClearTextUser}}
+
+
+
+ +
+ +
+ + + diff --git a/public/views/help/Hilfe.html b/public/views/help/Hilfe.html new file mode 100644 index 0000000..b728e8b --- /dev/null +++ b/public/views/help/Hilfe.html @@ -0,0 +1,36 @@ + + + + + + Document + + +

+Bilder, Symbole und Zeichnungen wurden durch KI generiert und teilweise aus bereits existierenden, lizenzierten Quellen im Auftrag der Stadt Frankfurt am Main verwendet. +

+ +

+Der Programmcode wurde ebenfalls durch KI unterstützend erstellt. Dies betrifft ausschließlich die Darstellungs- und Designlogik. Alle Verarbeitungen personenbezogener Daten liegen in der Obhut des nutzenden Betriebs. +

+ +

+Alle Inhalte der in RadixOS enthaltenen Plugins dienen ausschließlich dem dienstlichen Betrieb sowie der Weiterentwicklung interner Machine-Learning-Prozesse +(automatisiertes Lernen computergestützter Verfahren). +

+ +

+Datenbanken, Inhalte sowie personenbezogene Daten werden nicht an Dritte weitergegeben oder öffentlich zugänglich gemacht. RadixOS ist als geschlossenes System konzipiert. +

+ +

+ +Das Urheberrecht an – RADIX OS – liegt beim Entwickler + + +

+ + + + \ No newline at end of file diff --git a/public/views/integrated/development.hbs b/public/views/integrated/development.hbs new file mode 100644 index 0000000..8748991 --- /dev/null +++ b/public/views/integrated/development.hbs @@ -0,0 +1,11 @@ + + + + + + Document + + + test + + diff --git a/public/views/integrated/help.hbs b/public/views/integrated/help.hbs new file mode 100644 index 0000000..3692c8e --- /dev/null +++ b/public/views/integrated/help.hbs @@ -0,0 +1,106 @@ + + + + + Hilfe + + + + + +
+ +
+ + +
+
+
+
+
+
+ © Radix OS + Manuel Sowada +
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/public/views/integrated/serverconfig.hbs b/public/views/integrated/serverconfig.hbs new file mode 100644 index 0000000..63cc25c --- /dev/null +++ b/public/views/integrated/serverconfig.hbs @@ -0,0 +1,35 @@ +
+ +
+ + +
+
+
+
+
+ + + + + diff --git a/public/views/integrated/serverinfo.hbs b/public/views/integrated/serverinfo.hbs new file mode 100644 index 0000000..4e32335 --- /dev/null +++ b/public/views/integrated/serverinfo.hbs @@ -0,0 +1,101 @@ +
+
+ Dienst + + +
+ PID: + +
+
+
+
+ + + + + + + + + + + +
ErledigtTimestampUserNote
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/public/views/integrated/styleconfig.hbs b/public/views/integrated/styleconfig.hbs new file mode 100644 index 0000000..f46d2e6 --- /dev/null +++ b/public/views/integrated/styleconfig.hbs @@ -0,0 +1,30 @@ +
+ +
+ + +
+
+
+ + diff --git a/public/views/integrated/usersettings.hbs b/public/views/integrated/usersettings.hbs new file mode 100644 index 0000000..573a67d --- /dev/null +++ b/public/views/integrated/usersettings.hbs @@ -0,0 +1,113 @@ + +
+
+ + +
+ +
+ + +
+ + 18px +
+
+ +
+ + + +
+ + + \ No newline at end of file diff --git a/public/views/layouts/default.hbs b/public/views/layouts/default.hbs new file mode 100644 index 0000000..b12eedf --- /dev/null +++ b/public/views/layouts/default.hbs @@ -0,0 +1,9 @@ + + + + + + + {{{body}}} + + diff --git a/public/views/login.hbs b/public/views/login.hbs new file mode 100644 index 0000000..0e602fc --- /dev/null +++ b/public/views/login.hbs @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + +
+
+ Radix OS + + + + {{!--
--}} + +
+ + + + + +
+ +
+
+ + + + + + + + + + + + + + + diff --git a/public/views/partials/child.hbs b/public/views/partials/child.hbs new file mode 100644 index 0000000..44efd2a --- /dev/null +++ b/public/views/partials/child.hbs @@ -0,0 +1,16 @@ +
+
+
{{{viewLabel}}}
+
+ {{#if this.printable}} + + {{/if}} + +
+
+
+ {{{contentHtml}}} +
+
+
+ \ No newline at end of file diff --git a/public/views/partials/window.hbs b/public/views/partials/window.hbs new file mode 100644 index 0000000..3fb002c --- /dev/null +++ b/public/views/partials/window.hbs @@ -0,0 +1,19 @@ +
+
+ +
{{appname}} [{{label}}]
+
+ {{#if tutorial}} + + {{/if}} + + + +
+
+
+ {{{contentHtml}}} +
+
+
+ \ No newline at end of file diff --git a/public/views/plugindashboard.hbs b/public/views/plugindashboard.hbs new file mode 100644 index 0000000..e232c8a --- /dev/null +++ b/public/views/plugindashboard.hbs @@ -0,0 +1,242 @@ + + + + + + Plugin Dashboard + + + + +
+
+
+
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..b9fa7af --- /dev/null +++ b/server.js @@ -0,0 +1,253 @@ +//#region Modules +const { dirname } = require('path'); +const path = require('path'); +const https = require('https'); +var express = require('express'); +var app = express(); +var { create } = require('express-handlebars'); +const cookieParser = require('cookie-parser'); +var fs = require('fs'); +var os = require('os'); +var favicon = require('serve-favicon'); +const Sequelize = require('sequelize'); +const { Server } = require('socket.io'); +const { on } = require('cluster'); +// const { start } = require('repl'); +// const WebSocket = require('ws'); +//#endregion + + +require('module-alias/register'); // define paths in package.json +process.env.TZ = 'Europe/Berlin'; + +//#region Paths +app.locals.path = { + root: dirname(require.main.filename), + plugins: `${dirname(require.main.filename)}/plugins`, + public: `${dirname(require.main.filename)}/public`, + source: `${dirname(require.main.filename)}/src`, +} +//#endregion + + +//#region Server configuration +app.locals.stylesheet = JSON.parse(fs.readFileSync(`${app.locals.path.source}/models/stylesheet.json`, 'utf-8')); +app.locals.configuration = JSON.parse(fs.readFileSync(`${app.locals.path.source}/models/configuration.json`, 'utf-8')); +app.locals.package = JSON.parse(fs.readFileSync(`${app.locals.path.root}/package.json`, 'utf-8')); + +app.locals.startMenuItems = [ ]; + + + +(async () => { + // const server = https.createServer({ + // key: fs.readFileSync(`${app.locals.path.source}/secure/${app.locals.configuration.certificate.key}`), + // cert: fs.readFileSync(`${app.locals.path.source}/secure/${app.locals.configuration.certificate.chain}`), + // pfx: fs.readFileSync(`${app.locals.path.source}/secure/${app.locals.configuration.certificate.pfx}`), + // passphrase: "password", + // //cert: fs.readFileSync(`${app.locals.path.source}/secure/${app.locals.configuration.certificate.chain}`), + // }, app); +const securePath = `${app.locals.path.source}/secure`; +const certConfig = app.locals.configuration.certificate; + +let httpsOptions = {}; + +if (certConfig.pfx) { + httpsOptions = { + pfx: fs.readFileSync(`${securePath}/${certConfig.pfx}`), + passphrase: certConfig.passphrase + }; +} else { + httpsOptions = { + key: fs.readFileSync(`${securePath}/${certConfig.key}`), + cert: fs.readFileSync(`${securePath}/${certConfig.chain}`) + }; +} + +const server = https.createServer(httpsOptions, app); + + // const wss = new WebSocket.Server({ server }); + // wss.on('connection', socket => { + // socket.send('HELLO') + // }); + + + const io = new Server(server, { + pingTimeout: 60000, + maxHttpBufferSize: 1e8, // 100 MB + }); + + //#endregion + + + //#region Services/DatabaseModel + let service = new Map(); + let databaseModel = new Map(); + + let SocketManager = require(`@services/socketManager.js`); + let SqlManager = require(`@services/sqlManager.js`); + let EventManager = require(`@services/eventManager.js`); + let NotifyTrayManager = require(`@services/notifyTrayManager.js`); + let PluginManager = require(`@services/pluginManager.js`); + let FileSystemManager = require(`@services/fileSystemManager.js`); + let AuthenticationManager = require(`@services/authenticationManager.js`); + let ActiveDirectory = require(`@services/activeDirectoryManager.js`); + + service.set('socketManager', new SocketManager(io)); + await service.get('socketManager').addAsync('/'); + await service.get('socketManager').addAsync('admin'); + + service.set('sqlManager', new SqlManager()); + service.get('sqlManager').addInstance('main', app.locals.configuration.integration.sql.connect); + + databaseModel.set('eventlog', require(`${app.locals.path.source}/models/eventlogModel`)(service.get('sqlManager').getInstance('main'))); + databaseModel.set('eventlogView', require(`@models/eventlogView`)(service.get('sqlManager').getInstance('main'))); + service.set('eventManager', new EventManager(app, databaseModel.get('eventlog'), databaseModel.get('eventlogView'), service.get('socketManager'))); + + databaseModel.set('notifyTrayModel', require(`@models/notifyTrayModel`)(service.get('sqlManager').getInstance('main'))); + databaseModel.set('notifyTrayObjectModel', require(`@models/notifyTrayObjectsModel`)(service.get('sqlManager').getInstance('main'))); + databaseModel.set('notifyTrayView', require(`@models/notifyTrayView`)(service.get('sqlManager').getInstance('main'))); + service.set('notifyTray', new NotifyTrayManager(databaseModel.get('notifyTrayModel'), databaseModel.get('notifyTrayView'), databaseModel.get('notifyTrayObjectModel')) ); + + databaseModel.set('plugin', require(`@models/pluginModel`)(service.get('sqlManager').getInstance('main'))); + databaseModel.set('authentication', require(`@models/authenticationModel`)(service.get('sqlManager').getInstance('main'))); + + service.set('fileSystemManager', new FileSystemManager()); + service.set('authenticationManager', new AuthenticationManager(databaseModel.get('authentication'), app.locals.configuration.integration.token.secret, service.get('eventManager'))); + service.set('activeDirectoryManager', new ActiveDirectory(app.locals.configuration.integration.activedirectory)) + + // everytime last created service! + service.set('pluginManager', new PluginManager(app, databaseModel.get('plugin'), app.locals.path.plugins, app.locals.configuration.plugin.chown, service)); + + exports.databaseModel = databaseModel; + exports.service = service; + exports.path = app.locals.path; + //#endregion + + + //#region Service-Registration/Middleware/Utils/Helpers + require(`${app.locals.path.root}/utils.js`); + let helpers = service.get('fileSystemManager').loadAllFiles(`${app.locals.path.public}/helpers`, '.js'); + + app.use(express.urlencoded({ extended: true })); + app.use(express.json()); + app.use(cookieParser()); + app.use(favicon(`${app.locals.path.public}/images/radix_os_icon.ico`)); + + app.use(express.static(app.locals.path.root)); + app.use(express.static(app.locals.path.public)); + app.use(express.static(app.locals.path.source)); + + + + app.use(function(request, response, next) { + if (!request.secure) { + return response.redirect("https://" + request.headers.host + request.url + app.locals.configuration.server.port); + } + next(); // Http redirection to secure protocol + }) + //#endregion + + + //#region App config values + app.set('view engine', '.hbs'); + app.set('views', [ + `${app.locals.path.public}/views`, + `${app.locals.path.public}/views/integrated` + ]); + app.set('trust proxy', true) + //#endregion + + + //#region Error exception handling + app.on('uncaughtException', (err) => service.get('eventManager').write(null, 8, null, err )); + process.on('uncaughtException', (err) => service.get('eventManager').write(null, 8, null, err )); + process.on('unhandledRejection', (reason, promise) => service.get('eventManager').write(null, 8, null, reason )); + //#endregion + + + app.engine('hbs', create({ + extname: 'hbs', + helpers: helpers, + partialsDir: `${app.locals.path.public}/views/partials`, + layoutsDir: `${app.locals.path.public}/views/layouts`, + defaultLayout: `${app.locals.path.public}/views/layouts/default.hbs` + }).engine) + + + server.listen(app.locals.configuration.server.port, () => { + (async () => { + const databaseTest = await service.get('sqlManager').test("main"); // Check if database connection is established + service.get('eventManager').write(null, databaseTest.levelId, null, databaseTest.message); + + // Loading plugins + const plugins = await service.get('pluginManager').loadAll() + // const pluginsLoaded = { + // levelId: plugins.some(plugin => plugin.levelId > 0) ? 2 : 0, + // message: plugins.map(plugin => `${plugin.pluginName} v${plugin.metadata.version} ${plugin.message}`).join('
') + // } + // service.get('eventManager').write(null, pluginsLoaded.levelId, null, pluginsLoaded.message); + + plugins.forEach(plugin => { + service.get('eventManager').write(null, plugin.levelId, null, `${plugin.pluginName} v${plugin.metadata.version} ${plugin.message}`); + }); + + //#region Menu-Generator + app.use(async (req, res, next) => { + next(); + }); + //#endregion + + + //#region Implement routes + require(`${app.locals.path.source}/routes/indexRoutes.js`).route(app, service); + require(`${app.locals.path.source}/routes/loginRoutes.js`).route( + app, + service.get('authenticationManager'), + service.get('socketManager'), + service.get('eventManager') + ); + require(`${app.locals.path.source}/routes/adminRoutes.js`).route( + app, + service.get('authenticationManager'), + service.get('pluginManager'), + service.get('eventManager'), + service.get('socketManager'), + service.get('activeDirectoryManager'), + `${app.locals.path.source}/models/stylesheet.json` + ); + //#endregion + + + //#region Implements sockets + require(`${app.locals.path.source}/sockets/mainSocket.js`)( + app, + service.get('socketManager'), + '/', + service.get('pluginManager'), + databaseModel.get('authentication'), + service.get('fileSystemManager'), + service.get('eventManager'), + service.get('activeDirectoryManager') + ); + require(`${app.locals.path.source}/sockets/adminSocket.js`)( + app, + service.get('socketManager'), + 'admin', + service.get('eventManager') + ); + //#endregion + + })(); + + setTimeout(() => { + service.get('eventManager').write(null, 1, null, + `${app.locals.configuration.server.name} is running`, + `fqdn: https://${os.hostname()}:${app.locals.configuration.server.port}/`, + `process id: ${process.pid}`, + `url: ${os.hostname()}`, + `port: ${app.locals.configuration.server.port}` + ) + }, 1000); + }); +})(); diff --git a/skeleton.txt b/skeleton.txt new file mode 100644 index 0000000..68b3643 --- /dev/null +++ b/skeleton.txt @@ -0,0 +1,138 @@ +howto: +--> Watch out for names. Stylesheet and javascript names, will be used global! +----------------------------------------------------------------------------------------------------------------------- +├─ create new plugin: +│ ├─ add line to index.js: 'service.get('socketManager').add("%namespace%");' +│ ├─ add context in plugins route.js: +│ │ └─ app.post('/window/:name/index', async (req, res) => { await renderWindow(app, 'index', metadata, { user: await vUser.findOne({ where: { Benutzer: req.cookies.sAMAccountName }, raw: true } ) }, res) }); +│ └─ +├─ create new socket namespaces: +│ └─ service.get('socketManager').add("%namespace%"); +└─ add new startmenu entries: + ├─ add context object to file '/src/models/integratedStartmenuItems.js', where you can define pass through context paramters + └─ add view-template to '/public/views/integrated/' +----------------------------------------------------------------------------------------------------------------------- + +useful global features: +----------------------------------------------------------------------------------------------------------------------- +├─ %plugin%/public/javascripts.main.js ** for each plugin, main.js is loading for global use +├─ eventlog +│ ├─ service.get('eventManager).write(...) ** writes log entry and notifys admins on webui +│ └─ service.get('eventManager).writeLog(...) ** writes log entry only without firing webui message +├─ notifyTray +│ └─ service.get('notifyTray').createAndNotify ... +└─ +----------------------------------------------------------------------------------------------------------------------- + +rules: +----------------------------------------------------------------------------------------------------------------------- +├─ filesystem: +│ ├─ app descriptions, file/folder names in English; except proper names +│ ├─ files and folders always lowercase; continue in uppercase instead of spaces +│ ├─ files in routes folder, have to be named %name%Routes.js to generate dynamic menu +│ ├─ folder names in plural, if files are included +│ └─ folder names in singular, if files are excluded +├─ code: +| ├─ javascript: +| │ ├─ ids, classes, data-attributes (names and values) with hyphen (Bindestrich) instead of spaces; always lowercase +| │ └─ functions (and parameters in it) always lowercase; continue in uppercase instead of spaces +| ├─ socket.io: +| │ └─ socket names with underscores instead of spaces; always lowercase +| ├─ json: +| │ └─ variables in strings: "${}" (e.g ${ROOTPATH}) +└─ others: + ├─ admin eventlog in English + ├─ regions in English; first letter and proper names in capital + └─ users eventlog/messaging in German +----------------------------------------------------------------------------------------------------------------------- + + +folder structur: +----------------------------------------------------------------------------------------------------------------------- +root/ ** server relevant files +│ +├─ node_modules/ ** npm third party modules for nodejs server +├─ plugins/ ** server webapp based extensions +│ └─ %plugin_name%/ ** dynamic name reservation +│ ├─ views/ ** contains handlebar views +│ ├─ public/ ** contains plugin specified static files +│ │ ├─ helpers/ ** contains plugin specified handlebars helpers function +│ │ ├─ javascript/ ** contains plugin specified javascript code +│ │ ├─ others/ ** contains plugin specified files like options or templates +│ │ └─ styles/ ** contains plugin specified stylesheets +│ ├─ docs/ +│ │ ├─ help.hbs *** helper file for webapp description +│ │ └─ tutorial.hbs *** tutorial file, for webapp tutorial +│ ├─ index.js *** plugin entry point +│ ├─ plugin.json *** metadata basefile +│ └─ sockets.js *** plugin specified sockets +├─ public/ ** contains all visible static files +│ ├─ helpers/ ** javascript express handlebars helpers for client views +│ ├─ images/ ** global images +│ ├─ styles/ ** global stylesheets file +│ │ ├─ responsive/ +│ │ │ ├─ desktop.css *** responsive layout for desktops +│ │ │ ├─ mobile.css *** responsive layout for smartphones +│ │ │ └─ tablet.css *** responsive layout for tablets +│ ├─ dark.css *** dark theme +│ ├─ light.css *** light theme +│ └─ default.css *** global layout for everything everywhere all at once +│ └─ views/ ** contains the handlebars views +│ ├─ layouts/ ** templates for views +│ │ ├─ default.hbs *** main layout for everything everywhere all at once +│ │ └─ stricted_area.hbs *** layout without menus and links +│ └─ partials/ ** view snippets for multiple use +├─ src/ ** files for handle server inquiries +│ ├─ controllers/ ** contains the business logic receiving and validating client inquiries +│ ├─ models/ ** data/-base models +│ │ ├─ configuration.json *** server configuration file +│ │ └─ stylesheets.json *** global client stylesheet variabels +│ ├─ routes/ ** handles endpoint routing +│ ├─ secure/ ** contains encrypted passfiles and certificates +│ ├─ services/ ** contains business logic classes +│ │ ├─ authentication.js *** authenticates client connection attempts +│ │ ├─ encryption.js *** de-/ encryption serialization class +│ │ ├─ eventlog.js *** messaging class; client and server based +│ │ ├─ pluginsystem.js *** plugin engine +│ │ └─ sql.js *** microsoft sql connection and processing queries +│ └─ sockets/ ** global sockets +│ development.json *** development to do's +│ license_internal.txt *** license text file +│ package-lock.json *** npm link to node_modules +│ package.json *** file for server initialization +│ release_notes.json *** shows server version release notes +└─ server.js *** file for handle server start +----------------------------------------------------------------------------------------------------------------------- + + +stucture of style.json: +----------------------------------------------------------------------------------------------------------------------- +root +│ +├─ themes: +│ ├─ dark: +│ └─ light: +└─ responsive: + ├─ desktop: + ├─ tablet: + └─ mobile: +----------------------------------------------------------------------------------------------------------------------- + + +structure of plugin: +----------------------------------------------------------------------------------------------------------------------- +root/ +│ +├─ plugins/ +│ ├─ ${plugin}/ +│ │ ├─ views/ +│ │ │ └─ index.hbs (main file) +│ │ ├─ styles/ +│ │ │ └─ *.css (Set stylesheet as usual in header-section) +│ │ ├─ scripts/ +│ │ │ └─ *.js (Load file with function reloadPluginScript("/${pluginname}/javascript/${name}.js") in script-section) +│ │ └─ files/ +│ │ └─ *.png (Set the name exactly as it appears in the plugin.json file) +│ └─ plugin.json + +----------------------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/src/models/authenticationModel.js b/src/models/authenticationModel.js new file mode 100644 index 0000000..d4cb72f --- /dev/null +++ b/src/models/authenticationModel.js @@ -0,0 +1,84 @@ +const { DataTypes } = require('sequelize'); + + +module.exports = (sequelize) => { + const Authentication = sequelize.define('Authentication', { + ObjectGUID: { + type: DataTypes.UUID, + primaryKey: true, + allowNull: false, + }, + sAMAccountName: { + type: DataTypes.STRING, + allowNull: true, + }, + mail: { + type: DataTypes.STRING, + allowNull: true, + }, + givenName: { + type: DataTypes.STRING, + allowNull: true, + }, + sn: { + type: DataTypes.STRING, + allowNull: true, + }, + employeeID: { + type: DataTypes.STRING, + allowNull: true, + }, + title: { + type: DataTypes.STRING, + allowNull: true, + }, + department: { + type: DataTypes.STRING, + allowNull: true, + }, + streetAddress: { + type: DataTypes.STRING, + allowNull: true, + }, + userAccountControl_ID: { + type: DataTypes.STRING, + allowNull: true, + }, + authenticationType_ID: { + type: DataTypes.INTEGER, + allowNull: true, + }, + telephoneNumber: { + type: DataTypes.STRING, + allowNull: true, + }, + physicalDeliveryOfficeName: { + type: DataTypes.STRING, + allowNull: true, + }, + distinguishedName: { + type: DataTypes.STRING, + allowNull: true, + }, + password: { + type: DataTypes.STRING, + allowNull: true, // Passwort wird erst bei Erstanmeldung gesetzt + }, + refreshtoken: { + type: DataTypes.STRING, + allowNull: true, + }, + active: { + type: DataTypes.BOOLEAN, + allowNull: true, + }, + online: { + type: DataTypes.BOOLEAN, + allowNull: true, + }, + }, { + tableName: 'Authentication', // Tabellenname in der Datenbank + timestamps: false, // Falls du keine createdAt/updatedAt Spalten hast + }) + return Authentication; +}; \ No newline at end of file diff --git a/src/models/eventlogModel.js b/src/models/eventlogModel.js new file mode 100644 index 0000000..ec9a6fb --- /dev/null +++ b/src/models/eventlogModel.js @@ -0,0 +1,51 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + + async () => { + try { + await sequelize.authenticate(); + next(`database connection has been established successfully`); + } catch(error) { + next([`unable to connect to the database`, error]); + } + + } + const EventLog = sequelize.define('EventLog', { + ID: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true, // sorgt für Auto-Inkrement + }, + Message: { + type: DataTypes.STRING, + allowNull: false, + }, + Trace: { + type: DataTypes.STRING, + allowNull: true, + }, + Level_ID: { + type: DataTypes.INTEGER, + allowNull: false, + }, + PluginName: { + type: DataTypes.STRING, + allowNull: false, + }, + Date: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + ObjectGUID: { + type: DataTypes.UUID, + allowNull: false, + }, + }, { + tableName: 'EventLog', // Tabellenname in der Datenbank + timestamps: false, // Falls du keine createdAt/updatedAt Spalten hast + }) + + return EventLog; +}; \ No newline at end of file diff --git a/src/models/eventlogView.js b/src/models/eventlogView.js new file mode 100644 index 0000000..dca16ec --- /dev/null +++ b/src/models/eventlogView.js @@ -0,0 +1,34 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const EventLogView = sequelize.define('EventLogView', { + ID: { type: DataTypes.INTEGER, primaryKey: true }, + Message: { type: DataTypes.STRING }, + Trace: { type: DataTypes.STRING }, + Surname: { type: DataTypes.STRING }, + givenName: { type: DataTypes.STRING }, + Date: { type: DataTypes.DATE }, + department: { type: DataTypes.STRING }, + ClearTextUser: { type: DataTypes.STRING }, + ObjectTypDisplayName: { type: DataTypes.STRING }, + Level_ID: { type: DataTypes.INTEGER }, + LevelName: { type: DataTypes.STRING }, + LevelPriority: { type: DataTypes.INTEGER }, + LevelDisplayName: { type: DataTypes.STRING }, + PluginName: { type: DataTypes.STRING }, + ObjectGUID: { type: DataTypes.UUID }, + sAMAccountName: { type: DataTypes.STRING }, + mail: { type: DataTypes.STRING }, + Phone: { type: DataTypes.STRING }, + Office: { type: DataTypes.STRING }, + Adress: { type: DataTypes.STRING }, + authenticationType_ID: { type: DataTypes.INTEGER }, + TypeName: { type: DataTypes.STRING } + }, { + tableName: 'vEventLog', // dein SQL-View + timestamps: false, + freezeTableName: true + }); + + return EventLogView; +}; \ No newline at end of file diff --git a/src/models/integratedStartmenuItems.js b/src/models/integratedStartmenuItems.js new file mode 100644 index 0000000..b07dae2 --- /dev/null +++ b/src/models/integratedStartmenuItems.js @@ -0,0 +1,108 @@ +module.exports = ([ + { + section: "System", + name: 'Server', + active: true, + menu: { + label: 'Server', + items: [ + { + label: 'Styles', + view: 'styleconfig', + icon: 'brush.png', + permissions: [ 'Administration' ] + }, + { + label: 'Configs', + view: 'serverconfig', + icon: "app.png", + permissions: [ 'Administration' ] + } + ] + }, + }, + { + section: 'System', + name: 'EventLog', + active: true, + menu: { + label: 'EventLog', + items: [ + { + label: 'EventLog', + view: 'eventlog', + defaultSize: { width: "1200px", height: "1200px" }, + icon: "eventlog.ico", + permissions: [ 'Administration' ] + } + ] + } + }, + { + section: 'System', + name: 'Plugins', + active: true, + menu: { + label: 'Plugins', + items: [ + { + label: 'Plugins', + view: 'plugindashboard', + defaultSize: { width: "1000px", height: "400px" }, + icon: "plugins.png", + permissions: [ 'Administration' ] + } + ] + } + }, + { + section: 'System', + name: 'Info', + active: true, + menu: { + label: 'Info', + items: [ + { + label: 'Info', + view: 'serverinfo', + defaultSize: { width: "900px", height: "500px" }, + icon: "serverinfo.png", + permissions: [ 'Administration' ] + } + ] + } + }, + { + section: "Benutzer", + name: 'Einstellungen', + active: true, + menu: { + label: 'Einstellungen', + items: [ + { + label: 'Einstellungen', + view: 'usersettings', + defaultSize: { width: "460px", height: "515px" }, + icon: "app.png", + permissions: [ '*' ] + } + ] + } + }, + { + section: "Benutzer", + name: 'Hilfe', + active: true, + menu: { + label: 'Hilfe', + items: [ + { + label: 'Hilfe', + view: 'help', + icon: "help.png", + permissions: [ '*' ] + } + ] + } + } +]); diff --git a/src/models/notifyTrayModel.js b/src/models/notifyTrayModel.js new file mode 100644 index 0000000..2dde27a --- /dev/null +++ b/src/models/notifyTrayModel.js @@ -0,0 +1,40 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + + // // DB-Verbindung testen (optional) + // (async () => { + // try { + // await sequelize.authenticate(); + // console.log(`Database connection established for NotifyTray`); + // } catch (error) { + // console.error(`Unable to connect to the database`, error); + // } + // })(); + + const NotifyTray = sequelize.define('NotifyTray', { + ID: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true, // Identity + }, + ObjectGUID: { + type: DataTypes.UUID, + allowNull: false, + }, + NotifyTrayObject_ID: { + type: DataTypes.INTEGER, + allowNull: true + }, + SeenAt: { + type: DataTypes.DATE, + allowNull: true, + } + }, { + tableName: 'NotifyTray', + timestamps: false, + }); + + return NotifyTray; +}; \ No newline at end of file diff --git a/src/models/notifyTrayObjectsModel.js b/src/models/notifyTrayObjectsModel.js new file mode 100644 index 0000000..aa59517 --- /dev/null +++ b/src/models/notifyTrayObjectsModel.js @@ -0,0 +1,39 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const NotifyTrayObjects = sequelize.define('NotifyTrayObjects', { + ID: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true, // manuell zu vergeben + }, + PluginName: { + type: DataTypes.STRING(50), + allowNull: true, + }, + Message: { + type: DataTypes.TEXT, + allowNull: false, + }, + JSON: { + type: DataTypes.TEXT, // große JSON-Strings + allowNull: true, + }, + ActionRequired: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + CreatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + } + }, { + tableName: 'NotifyTrayObjects', + timestamps: false, + }); + + return NotifyTrayObjects; +}; \ No newline at end of file diff --git a/src/models/notifyTrayView.js b/src/models/notifyTrayView.js new file mode 100644 index 0000000..befd171 --- /dev/null +++ b/src/models/notifyTrayView.js @@ -0,0 +1,70 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const NotifyTrayView = sequelize.define('vNotifyTray', { + ID: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + }, + active: { + type: DataTypes.BOOLEAN, + allowNull: true, + }, + online: { + type: DataTypes.BOOLEAN, + allowNull: true, + }, + ObjectGUID: { + type: DataTypes.STRING, + allowNull: true, + }, + + sAMAccountName: { + type: DataTypes.STRING, + allowNull: true, + }, + givenName: { + type: DataTypes.STRING, + allowNull: true, + }, + sn: { + type: DataTypes.STRING, + allowNull: true, + }, + mail: { + type: DataTypes.STRING, + allowNull: true, + }, + PluginName: { + type: DataTypes.STRING(50), + allowNull: true, + }, + JSON: { + type: DataTypes.TEXT, + allowNull: true, + }, + ActionRequired: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + CreatedAt: { + type: DataTypes.DATE, + allowNull: true, + }, + Message: { + type: DataTypes.TEXT, + allowNull: true, + }, + SeenAt: { + type: DataTypes.DATE, + allowNull: true, + } + }, { + tableName: 'vNotifyTray', // 👈 WICHTIG: View statt Tabelle + timestamps: false, + }); + + return NotifyTrayView; +}; \ No newline at end of file diff --git a/src/models/pluginModel.js b/src/models/pluginModel.js new file mode 100644 index 0000000..5bb92ef --- /dev/null +++ b/src/models/pluginModel.js @@ -0,0 +1,24 @@ +const { DataTypes } = require('sequelize'); + + +module.exports = (sequelize) => { + const Plugin = sequelize.define('Plugin', { + Name: { + type: DataTypes.STRING(50), + allowNull: false, + primaryKey: true + }, + Active: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + Version: { + type: DataTypes.STRING(25), + } + }, { + tableName: 'Plugins', // Tabellenname in der Datenbank + timestamps: false, // Falls du keine createdAt/updatedAt Spalten hast + }); + + return Plugin; +}; diff --git a/src/models/releasenotes.json b/src/models/releasenotes.json new file mode 100644 index 0000000..22a4bac --- /dev/null +++ b/src/models/releasenotes.json @@ -0,0 +1,58 @@ +[ + { + "sAMAccountName": "manuel.sowada", + "plugin": "System", + "datetime": "2026-02-11", + "value": "HTML-Tabellen werden durch virtuelles Dataset abgebildet", + "finish": true + }, + { + "sAMAccountName": "manuel.sowada", + "plugin": "System", + "datetime": "2026-04-14", + "value": "Bubble-Notify eingefügt", + "finish": true + }, + { + "sAMAccountName": "manuel.sowada", + "plugin": "System", + "datetime": "2026-04-16", + "value": "Window drag 'n drop bugfixes", + "finish": true + }, + { + "sAMAccountName": "manuel.sowada", + "plugin": "System", + "datetime": "2026-04-17", + "value": "OS-Logik und -Design Bugfixes + mobile-responsive-design", + "finish": true + }, + { + "sAMAccountName": "manuel.sowada", + "plugin": "System", + "datetime": "2026-04-18", + "value": "Speichern der Window-Payloads pro Benutzer, divers Bugfixes und Verbesserungen in der OS-Logik", + "finish": true + }, + { + "sAMAccountName": "manuel.sowada", + "plugin": "System", + "datetime": "2026-04-19", + "value": "PluginManager erweitert, Hilfe in Tabs umbgebaut, diverse Design-Bugfixes", + "finish": true + }, + { + "sAMAccountName": "manuel.sowada", + "plugin": "System", + "datetime": "2026-04-20", + "value": "OS-Logik update: fetch ersetzt socket.io bei open_window und open_view", + "finish": true + }, + { + "sAMAccountName": "manuel.sowada", + "plugin": "System", + "datetime": "2026-04-21", + "value": "Plugin-System + JSONtree bugfixes", + "finish": true + } +] diff --git a/src/routes/adminRoutes.js b/src/routes/adminRoutes.js new file mode 100644 index 0000000..a8b4888 --- /dev/null +++ b/src/routes/adminRoutes.js @@ -0,0 +1,175 @@ +const { exec } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const { service } = require('@root/server.js'); + + +const configurationFile = path.join(require('@root/server.js').path.source, 'models', 'configuration.json'); +const stylesheetFile = path.join(require('@root/server.js').path.source, 'models', 'stylesheet.json'); +const serverInfoFile = path.join(require('@root/server.js').path.root, 'package.json'); + + +module.exports = { + route(app, authenticationManager, pluginManager, eventManager, socketManager, activeDirectoryManager, stylesheetJson) { + // JSON configuration abrufen + app.post('/api/getConfig', (req, res) => { + fs.readFile(configurationFile, 'utf8', (err, data) => { + if (err) return res.status(500).send(err); + res.status(200).send(JSON.parse(data)); + }); + }); + + // JSON configuration speichern + app.post('/config', (req, res) => { + fs.writeFile(configurationFile, JSON.stringify(req.body, null, 2), (err) => { + if (err) return res.status(500).send(err); + res.status(200).send({ status: 'ok' }); + }); + }); + + // JSON stylesheet abrufen + app.post('/api/getStyles', (req, res) => { + fs.readFile(stylesheetFile, 'utf8', (err, data) => { + if (err) return res.status(500).send(err); + res.status(200).send(JSON.parse(data)); + }); + }); + + // JSON stylesheet speichern + app.post('/style', (req, res) => { + fs.writeFile(stylesheetFile, JSON.stringify(req.body, null, 2), (err) => { + if (err) return res.status(500).send(err); + res.status(200).send({ status: 'ok' }); + }); + }); + + + // JSON package.json abrufen + app.post('/api/getServerInfo', (req, res) => { + fs.readFile(serverInfoFile, 'utf8', (err, data) => { + if (err) { return res.status(500).send(err); } + res.status(200).send({ package: JSON.parse(data), pid: process.pid, releaseNotes: global.json.releaseNotes.live }); + }); + }); + + // JSON package.json speichern + app.post('/serverinfo', (req, res) => { + fs.writeFile(serverInfoFile, JSON.stringify(req.body, null, 2), (err) => { + if (err) return res.status(500).send(err); + res.status(200).send({ status: 'ok' }); + }); + }); + + + app.post('/api/eventlog/clearlog', (req, res) => { + eventManager.clear(); + res.status(200).send({ status: 'ok' }) + }) + + app.post('/api/eventlog/getLogs', async (req, res) => { + res.status(200).json(await service.get('eventManager').getAllEventLogs()); + }) + + + app.post('/api/shutdown', (req, res) => { + exec(`kill -9 ${process.pid}`, (error, stdout, stderr) => { + if (error) { + service.get('eventManager').write(req.cookies.ObjectGUID, 4, null, error.message); + return res.status(500).send({ status: 'error', message: error.message }); + } + service.get('eventManager').write(req.cookies.ObjectGUID, 0, null, `Server heruntergefahren`); + res.status(200).send({ status: 'ok' }); + }); + }); + + app.post('/api/restart', (req, res) => { + service.get('eventManager').write(req.cookies.ObjectGUID, 2, null, `Der Neustart ist noch nicht implementiert`); + res.status(200).send({ status: 'ok' }); + // exec(`kill -9 ${process.pid}`, (error, stdout, stderr) => { + // if (error) { + // service.get('eventManager').write(req.cookies.ObjectGUID, 4, null, error.message); + // return res.status(500).send({ status: 'error', message: error.message }); + // } + // service.get('eventManager').write(req.cookies.ObjectGUID, 0, null, `Server neu gestartet`); + // res.status(200).send({ status: 'ok' }); + // }); + }); + + + app.post('/api/plugins/activation', async (req, res) => { + const { name, state } = req.body; + let result = null; + if(state) { + result = await pluginManager.load(name, true); + } else { + result = await pluginManager.unload(name); + } + console.log(result) + // result = { ...result, authorized: result.metadata.permissions.some(async permission => { await activeDirectoryManager.getGroup(permission) != null && await activeDirectoryManager.isUserMemberOfRecursive(req.cookies.sAMAccountName, permission)}) } + + eventManager.write(null, result.levelId, name, result.message); + // socketManager.broadcast('admin', 'plugin_status', result); + socketManager.broadcast('/', 'plugin_status', result); + res.status(200).json(result); + }); + + app.post('/api/plugins/getAll', async (req, res) => { + try { + const plugins = await pluginManager.getStatus(); + res.status(200).json(plugins); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + + app.post('/api/plugins/:name/update', async (req, res) => { + const { name } = req.params; + try { + const { updates } = req.body; + const result = await pluginManager.update(name, updates); + // result = { ...result, authorized: result.metadata.permissions.some(async permission => { await activeDirectoryManager.getGroup(permission) != null && await activeDirectoryManager.isUserMemberOfRecursive(req.cookies.sAMAccountName, permission)}) } + + service.get('eventManager').writeLog(req.cookies.ObjectGUID, result.levelId, name, result.message); + // socketManager.broadcast('admin', 'plugin_status', result); + // socketManager.broadcast('/', 'plugin_status', result); + res.status(200).json(result); + } catch (error) { + service.get('eventManager').write(req.cookies.ObjectGUID, 4, name, `Fehler beim Aktualisieren des Plugins: ${error}`); + res.status(500).json({ error: error.message }); + } + }); + + app.post('/api/plugins/:name/rename', async (req, res) => { + const { name } = req.params; + const { newName } = req.body; + const result = await service.get('pluginManager').rename(name, newName); + + // const result = { levelId: 0, pluginName: name, message: `Plugin erstellt` }; + // await pluginManager.create(name); + // res.status(200).json(result); + + eventManager.writeLog(null, result.levelId, name, result.message); + // socketManager.broadcast('admin', 'plugin_status', result); + }); + + app.post('/api/plugins/:name/create', async (req, res) => { + const { name } = req.params; + const result = { levelId: 0, pluginName: name, message: `Plugin erstellt` }; + await pluginManager.create(name); + + eventManager.writeLog(null, result.levelId, name, result.message); + res.status(200).json(result); + // socketManager.broadcast('admin', 'plugin_status', result); + }); + + app.post('/admin/plugins/:name/delete', async (req, res) => { + const { name } = req.params; + const result = { status: 'delete', pluginName: name, levelId: 0, message: `Plugin ${name} gelöscht` }; //await pluginManager.delete(name); + res.status(200).json(result); + + eventManager.write(null, result.levelId, name, result.message); + socketManager.broadcast('admin', 'plugin_status', result); + }); + } +}; diff --git a/src/routes/indexRoutes.js b/src/routes/indexRoutes.js new file mode 100644 index 0000000..1d66fad --- /dev/null +++ b/src/routes/indexRoutes.js @@ -0,0 +1,125 @@ +const fs = require('fs'); +const path = require('path'); + +const { renderWindow } = require('@services/renderWindow.js'); +const { service } = require('@root/server.js'); +const { File: HotReload } = require(`@services/hotReload.js`); + +const { doesNotReject } = require('assert'); +// let startMenuItemContext = require('@models/integratedStartmenuItems.js') + + +module.exports = { + route: function(app, service) { + app.get('/', service.get('authenticationManager').authenticate(), async (req, res) => { + const startMenuItems = await global.startMenuItems( + app, + req.cookies.sAMAccountName, + service + ); + res.render('desktop', { layout: 'default', startMenuItems: startMenuItems }); + }); + + + app.post('/api/open_app', (req, res) => { + const { name, view, viewLabel, location, size, state, zIndex } = req.body; + + const pluginPath = path.join(global.path.plugins, name, 'plugin.json'); + + let context = fs.existsSync(pluginPath) + ? service.get('fileSystemManager').loadJSON(pluginPath) + : (global.json.startMenuItems.live).find(item => item.name == name); + + context.defaultSize = + context.menu.items.find(item => item.label == viewLabel)?.defaultSize || + { width: 800, height: 600 }; + + delete context.config; + res.json({ name, view, viewLabel, context, location, size, state, zIndex }); + }); + + + + app.get('/api/NotifyTray/getTrays', async (req, res) => { + const objectGuid = req.cookies.ObjectGUID; + // console.log(await service.get('notifyTray').getOpenNotifications(objectGuid)) + res.status(200).json(await service.get('notifyTray').getOpenNotifications(objectGuid)); + }) + + app.post('/api/NotifyTray/markAsSeen', async (req, res) => { + const objectGuid = req.cookies.ObjectGUID; + const notificationId = req.body.id; + const notificationValue = req.body.value; + await service.get('notifyTray').markAsSeen(objectGuid, notificationId, notificationValue); + res.status(204).send(); + }) + + app.post('/api/Plugins/loadScripts', service.get('authenticationManager').authenticate(), async(req, res) => { + const scripts = service.get('pluginManager').getStatus().map(plugin => { + + const exists = service.get('fileSystemManager').exists(path.join(plugin.pluginPath, 'public', 'javascript', 'main.js')) + if (exists && exists.status) { + return path.join('/', plugin.name, 'javascript', 'main.js'); + } + return null; + }) + .filter(script => script !== null); + res.status(200).send(scripts); + }); + + + + + + + app.post('/window/EventLog/eventlog', async (req, res) => { + await renderWindow(app, 'eventlog.hbs', { }, { }, res) + }); + + app.post('/window/Server/styleconfig', async (req, res) => { + await renderWindow(app, 'styleconfig.hbs', { }, { }, res) + }); + + app.post('/window/Plugins/plugindashboard', async (req, res) => { + await renderWindow(app, 'plugindashboard.hbs', { }, { }, res) + }); + + app.post('/window/Info/serverinfo', async (req, res) => { + await renderWindow(app, 'serverinfo.hbs', { }, { }, res) + }); + + app.post('/window/Server/serverconfig', async (req, res) => { + await renderWindow(app, 'serverconfig.hbs', { }, { }, res) + }); + + app.post('/window/Einstellungen/usersettings', async (req, res) => { + await renderWindow(app, 'usersettings.hbs', { }, { }, res) + }); + + app.post('/window/Hilfe/help', async (req, res) => { + await renderWindow(app, 'help.hbs', { }, { }, res) + }); + + app.get('/api/help/getTabs', async (req, res) => { + const tabNames = (await startMenuItems(app, req.cookies.sAMAccountName)) + .filter(plugin => plugin.active && plugin.menu.items.some(i => i.authorized)) + .map(plugin => ({ name: plugin.menu.label })); + res.status(200).json(tabNames); + }); + + app.post('/api/help/getHelp', async (req, res) => { + const { name } = req.body; + const props = (await startMenuItems(app, req.cookies.sAMAccountName)) + .filter(plugin => plugin.name === name && plugin.active && plugin.menu.items.some(i => i.authorized)) + .map(async plugin => ( + { + name: plugin.menu.label, + version: plugin.version, + description: plugin.description, + html: await fs.promises.readFile(plugin.section === 'Plugin' ? path.join(plugin.pluginPath, 'docs', 'help.html') : path.join(app.locals.path.public, 'views', 'help', plugin.name + '.html'), 'utf-8') + } + )); + res.status(200).send(await props[0]); + }); + } +}; \ No newline at end of file diff --git a/src/routes/loginRoutes.js b/src/routes/loginRoutes.js new file mode 100644 index 0000000..8419e3d --- /dev/null +++ b/src/routes/loginRoutes.js @@ -0,0 +1,79 @@ +const { verify } = require("jsonwebtoken"); + +module.exports = { + route(app, authenticationManager, socketManager, eventManager) { + app.get(`/login`, (req, res) => { + res.render(`login`, { layout: 'default' }); + }) + + app.post('/login', async (req, res) => { + const { sAMAccountName, password } = req.body; + const userModel = await authenticationManager.Authentication.findOne({ + where: { sAMAccountName: sAMAccountName }, attributes: ['ObjectGUID'], + raw: true + }); + const objectGuid = userModel !== null ? userModel.ObjectGUID : sAMAccountName; + try { + // set safety cookies + res.cookie('sAMAccountName', sAMAccountName, { + httpOnly: false, + secure: true, + sameSite: 'Strict', + maxAge: 1000 * 60 * 60 * 24 * 365 + }) + + res.cookie('ObjectGUID', objectGuid, { + httpOnly: false, + secure: true, + sameSite: 'Strict', + maxAge: 1000 * 60 * 60 * 24 * 365 + }) + const login = await authenticationManager.login(sAMAccountName, password); + + eventManager.writeLog(objectGuid, login.levelId, null, login.message); + res.status(login.levelId == 0 ? 200 : 401).json(login); + } catch (err) { + res.status(500).json(login); + } + }); + + + // Geschützte Route + app.get('/me', authenticationManager.authenticate(), (req, res) => { + res.json(JSON.stringify({ + user: { + name: req.user + } + }, null, 4)); + }); + + app.post('/checkLoginName', async (req, res) => { + const { sAMAccountName } = req.body; + + const userExists = await authenticationManager.Authentication.findOne({ where: { sAMAccountName: sAMAccountName } }); + const auth = { objectGuid: userExists != null ? userExists.ObjectGUID : sAMAccountName, sAMAccountName: sAMAccountName }; + res.status(userExists ? 200 : 404).json({ exists: userExists != null }); + }); + + app.get('/verifying', async (req, res, next) => { + const verify = await authenticationManager.verifyUserToken(); + eventManager.write(req.user.objectGuid, verify.levelId, null, verify.message); + next(); + }); + + // Logout + app.post('/logout', authenticationManager.authenticate(), async (req, res) => { + const logout = await authenticationManager.logout(req.user.sAMAccountName); + + socketManager.sendTo('/', req.user.objectGuid, 'login_status', { levelId: logout.levelId, message: logout.message } ) + eventManager.write(req.user.objectGuid, logout.levelId, null, logout.message); + + res.clearCookie('sAMAccountName'); + res.clearCookie('ObjectGUID'); + + setTimeout(() => res.render('login', { layout: false, title: app.locals.configuration.server.name }), 3000); + // res.json({ message: 'Logout erfolgreich' }); + }); + } +}; + diff --git a/src/services/activeDirectoryManager.js b/src/services/activeDirectoryManager.js new file mode 100644 index 0000000..3229adf --- /dev/null +++ b/src/services/activeDirectoryManager.js @@ -0,0 +1,258 @@ +const ActiveDirectory = require('activedirectory2'); + +class ActiveDirectoryManager { + constructor({ + url, + baseDN, + username, + password, + userAttributes, + groupAttributes, + computerAttributes + }) { + this.ad = new ActiveDirectory({ + url, + baseDN, + username, + password, + attributes: { + user: userAttributes, + group: groupAttributes, + computer: computerAttributes + } + }); + + this.userAttributes = userAttributes; + this.groupAttributes = groupAttributes; + this.computerAttributes = computerAttributes; + } + + /** + * ----------------------------------------------------- + * INTERNAL GENERIC LDAP SEARCH + * ----------------------------------------------------- + */ + async ldapSearch(options) { + return new Promise((resolve, reject) => { + this.ad.find(options, (err, result) => { + if (err) return reject(err); + resolve(result || {}); + }); + }); + } + + /** + * ----------------------------------------------------- + * USER FUNCTIONS + * ----------------------------------------------------- + */ + + async getUser(username, attributes = this.userAttributes) { + return new Promise((resolve, reject) => { + this.ad.findUser({ attributes }, username, (err, user) => { + if (err) return reject(err); + resolve(user || null); + }); + }); + } + + async getUserDN(username) { + const user = await this.getUser(username); + return user?.dn || null; + } + + async findUsers(query, attributes = this.userAttributes) { + return new Promise((resolve, reject) => { + const filter = `(&(objectClass=user)(|(cn=${query})(sAMAccountName=${query})(mail=${query})(displayName=${query})))`; + + this.ad.findUsers({ filter, attributes }, (err, users) => { + if (err) return reject(err); + resolve(users || []); + }); + }); + } + + /** + * ----------------------------------------------------- + * GROUP FUNCTIONS + * ----------------------------------------------------- + */ + + async getGroup(groupName, attributes = this.groupAttributes) { + return new Promise((resolve, reject) => { + this.ad.findGroup({ attributes }, groupName, (err, group) => { + if (err) return reject(err); + resolve(group || null); + }); + }); + } + + async findGroups(query, attributes = this.groupAttributes) { + return new Promise((resolve, reject) => { + const filter = `(&(objectClass=group)(cn=${query}))`; + + this.ad.findGroups({ filter, attributes }, (err, groups) => { + if (err) return reject(err); + resolve(groups || []); + }); + }); + } + + /** + * ----------------------------------------------------- + * COMPUTER / OU FUNCTIONS 🖥️ + * ----------------------------------------------------- + */ + + /** + * Einzelnen Computer holen + */ + async getComputer(name, attributes = this.computerAttributes) { + return new Promise((resolve, reject) => { + const filter = `(&(objectClass=computer)(|(cn=${name})(dNSHostName=${name})))`; + + this.ad.find({ filter, attributes }, (err, result) => { + if (err) return reject(err); + resolve(result?.other?.[0] || null); + }); + }); + } + + + /** + * Alle Computer + */ + async getComputers(attributes = this.computerAttributes) { + const options = { + baseDN: this.ad.baseDN, + filter: '(objectClass=computer)', + attributes + }; + + const result = await this.ldapSearch(options); + return result.other || []; + } + + + /** + * Alle Computer aus einer OU holen + */ + async getComputersFromOU(ouDn, attributes = this.computerAttributes) { + const options = { + baseDN: ouDn, + filter: '(objectClass=computer)', + attributes + }; + + const result = await this.ldapSearch(options); + return result.other || []; + } + + /** + * Computer suchen (Wildcard möglich) + * Beispiele: "PC-*", "*LAPTOP*", "SRV01" + */ + async findComputers(query, attributes = this.computerAttributes) { + const filter = `(&(objectClass=computer)(|(cn=${query})(dNSHostName=${query})))`; + + const result = await this.ldapSearch({ + filter, + attributes + }); + return result.other || []; + } + + /** + * ----------------------------------------------------- + * GROUP MEMBERSHIP (DIRECT & RECURSIVE) + * ----------------------------------------------------- + */ + + async isUserMemberOfDirect(username, groupName) { + return new Promise((resolve, reject) => { + this.ad.isUserMemberOf(username, groupName, (err, isMember) => { + if (err) return reject(err); + resolve(isMember); + }); + }); + } + + async isUserMemberOfRecursive(username, groupName, visited = new Set()) { + const key = groupName.toLowerCase(); + if (visited.has(key)) return false; + visited.add(key); + + const direct = await this.isUserMemberOfDirect(username, groupName); + if (direct) return true; + + const group = await this.getGroup(groupName); + if (!group || !Array.isArray(group.member)) return false; + + for (const dn of group.member) { + const match = dn.match(/CN=([^,]+)/i); + if (!match) continue; + + const subGroupName = match[1]; + const found = await this.isUserMemberOfRecursive(username, subGroupName, visited); + if (found) return true; + } + + return false; + } + + async getGroupSubgroups(groupName, visited = new Set()) { + const key = groupName.toLowerCase(); + if (visited.has(key)) return []; + + visited.add(key); + + const group = await this.getGroup(groupName); + if (!group || !Array.isArray(group.member)) return []; + + const results = []; + + for (const memberDN of group.member) { + const match = memberDN.match(/CN=([^,]+)/i); + if (!match) continue; + + const subGroupName = match[1]; + const sub = await this.getGroup(subGroupName).catch(() => null); + if (!sub) continue; + + results.push(sub); + results.push(...await this.getGroupSubgroups(subGroupName, visited)); + } + + return results; + } + + async getGroupRecursive(groupName, visited = new Set()) { + const key = groupName.toLowerCase(); + if (visited.has(key)) return null; + + visited.add(key); + + const group = await this.getGroup(groupName); + if (!group) return null; + + const result = { + ...group, + subgroups: [] + }; + + if (!Array.isArray(group.member)) return result; + + for (const memberDN of group.member) { + const match = memberDN.match(/CN=([^,]+)/i); + if (!match) continue; + + const subGroupName = match[1]; + const subTree = await this.getGroupRecursive(subGroupName, visited); + if (subTree) result.subgroups.push(subTree); + } + + return result; + } +} + +module.exports = ActiveDirectoryManager; diff --git a/src/services/authenticationManager.js b/src/services/authenticationManager.js new file mode 100644 index 0000000..eb9263b --- /dev/null +++ b/src/services/authenticationManager.js @@ -0,0 +1,172 @@ +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); + +let { levelId, message } = ''; + +/** + * Authentication class for login method, token validation and password setting + */ +class AuthenticationManager { + /** + * + * @param {object} model - Use the authentication database model for interact with the database + * @param {string} secretKey - Defines the server secret for token validation + */ + constructor(model, secretKey, eventManager) { + this.eventManager = eventManager; + + // if (!model) throw new Error('Sequelize Model wird benötigt'); + // if (!secretKey) throw new Error('Secret Key wird benötigt'); + + this.Authentication = model; + this.SECRET_KEY = secretKey; + } + + /** + * Set or reset password of user + * @param {string} sAMAccountName - Windows account name + * @param {string} password - Set the new password + */ + async setPassword(sAMAccountName, password) { + const user = await this.Authentication.findOne({ where: { sAMAccountName } }); + if (!user) { + // this.eventManager.write(null, 2, 0, { aboveLevel: 1 }, `User nicht gefunden`); + levelId = 2; + message = `Unbekannter User` + return {token: null, levelId: levelId }; + // throw new Error(`User ${sAMAccountName} nicht gefunden`); + } + // if (user.password) throw new Error('Passwort bereits gesetzt'); + + const hashedPassword = await bcrypt.hash(password, 10); + user.password = hashedPassword; + await user.save(); + } + + /** + * Login mit Speicherung des Tokens in der Datenbank + */ + async login(sAMAccountName, password) { + const user = await this.Authentication.findOne({ where: { sAMAccountName } }); + + if (!user) { + //this.eventManager.write(null, 2, null, null, `User ${sAMAccountName} nicht geufunden`) + levelId = 2; + message = `Unbekannter Benutzer`; + return { token: null, levelId: levelId, message: message }; + // throw new Error('Unkown user'); + } + if (!user.password) { + this.setPassword(sAMAccountName, password); + // this.eventManager.write(user.ObjectGUID, 2, null, null, 'User registration initialized') + levelId = 1; + message = `Benutzer nicht registiert`; + return { token: null, levelId: levelId, message: message }; + // throw new Error('User not registered'); + } + + const passwordMatch = await bcrypt.compare(password, user.password); + if (!passwordMatch) { + // this.eventManager.write(user.ObjectGUID, 2, null, null, 'Password doesn\'t match'); + levelId = 2; + message = `Falsches Passwort`; + return { token: null, levelId: levelId, message: message }; + // throw new Error('Wrong password'); + } + + // Token erzeugen + const payload = { + sAMAccountName: user.sAMAccountName, + mail: user.mail, + givenName: user.givenName, + sn: user.sn + }; + + const token = jwt.sign(payload, this.SECRET_KEY, { expiresIn: '100y' }); + // Token in DB speichern + user.refreshtoken = token; + user.online = true; + await user.save(); + + // this.eventManager.write(user.ObjectGUID, 1, null, null, 'Erfolgreich angemeldet'); + levelId = 0; + message = `Erfolgreich angemeldet`; + return { token: token, levelId: levelId, message: message }; + } + + /** + * Logout löscht Token aus der DB + */ + async logout(sAMAccountName) { + const user = await this.Authentication.findOne({ where: { sAMAccountName } }); + if (user) { + user.refreshtoken = null; + user.online = false; + await user.save(); + levelId = 0; + message = `Erfolgreich abgemeldet`; + return { token: null, levelId: levelId, message: message }; + } + } + + /** + * Token-Prüfung (über DB) + */ + async verifyUserToken(sAMAccountName) { + const user = await this.Authentication.findOne({ where: { sAMAccountName } }); + if (!user || !user.refreshtoken) { + levelId = 1, + message = `Kein gültiger Token`; + // throw new Error('Kein gespeicherter Token gefunden'); + } + + try { + const payload = jwt.verify(user.refreshtoken, this.SECRET_KEY); + levelId = 0; + message = `User verifiziert`; + return { valid: true, payload, user, levelId: levelId, message: message } + } catch { + levelId = 4; + message = `Ungültiger Token`; + return { valid: false, payload, user, levelId: levelId, message: message } + } + } + + /** + * Express Middleware – prüft Token direkt aus DB anhand sAMAccountNamec + */ + authenticate() { + return async (req, res, next) => { + try { + const sAMAccountName = req.cookies?.sAMAccountName; + const objectGUID = req.cookies?.ObjectGUID; + if (!sAMAccountName || !objectGUID) { + return res.redirect('/login'); + // return res.status(401).json({ message: 'Kein Benutzer-Cookie gefunden' }); + } + + const user = await this.Authentication.findOne({ where: { sAMAccountName } }); + if (!user || !user.refreshtoken) { + return res.redirect('/login'); + // return res.status(401).json({ message: 'Benutzer oder Token nicht gefunden' }); + } + + if (user.active === false) { + return res.redirect('/login'); + // return res.status(401).json({ message: 'Benutzer ist nicht aktiv' }); + } + + // Token aus DB prüfen + const payload = jwt.verify(user.refreshtoken, this.SECRET_KEY); + req.user = user; + next(); + } catch (err) { + console.error(err); + return res.redirect('/login'); + // res.status(401).json({ message: 'Authentifizierung fehlgeschlagen' }); + } + }; + } +} + +module.exports = AuthenticationManager; diff --git a/src/services/eventManager.js b/src/services/eventManager.js new file mode 100644 index 0000000..292e7de --- /dev/null +++ b/src/services/eventManager.js @@ -0,0 +1,159 @@ +const { Op } = require('sequelize'); +/** + * Custom event logging class + * + */ +class EventManager { + /** + * @param {*} eventlogModel - Use the eventlog database model for interact with the database + * @param {*} socketManager - Get the administration socket for sending events to frontend + */ + constructor(app, eventlogModel, eventLogView, socketManager) { + this.app = app; + this.EventLog = eventlogModel; + this.socketManager = socketManager; + this.EventLogView = eventLogView; + } + + /** + * Creates a new log entry in EventLog database table + * @param {string} [objectGuid] - MSSQL uniqueidentifier (UUID) or null = '00000000-0000-0000-0000-000000000000' AS 'SYSTEM' + * @param {number} levelId - -1=test, 0=success, 1=log, 2=warn, 4=error, 8=throw_exception + * @param {number} pluginId - ID of plugin + * @param {Object} socketSending - Use socket to send error message to admin web frontends. Object: { active: true, levelId: 1 } + * @param {Array} args - Message: string or comma seperated array of string + * args[0].stack !== undefined - => Client side (error) stack and message + */ + async write(objectGuid, levelId, pluginName = null, ...args) { + const err = new Error(); + const stackLine = args[0].stack !== undefined ? args[0].stack : err.stack.split('\n')[2]; // calls trace-line + //const trace = stackLine.split("\n") !== undefined ? stackLine.split("\n")[1].trim() : stackLine.match(/\/.*\d+/)[0].replace(this.app.locals.path.root, '') || ''; // path:line:column + const trace = stackLine?.split("\n")?.[1]?.trim() ?? stackLine?.match(/\/.*\d+/)?.[0]?.replace(this.app.locals.path.root, '') ?? ''; + + // const message = !Array.isArray(...args) ? [...args][0] : [...args][0][0] + const message = args[0].stack !== undefined ? args[0].message : args.join('\r\n\t'); + + const convertLevel = levelId == -1 ? 'test' : levelId == 0 ? 'success' : levelId == 1 ? 'log' : levelId == 2 ? 'warn' : levelId == 4 ? 'error' : 'throw exception'; + const convertPluginName = pluginName == null ? 'SYSTEM' : pluginName; + + const entry = await this.EventLog.create({ + Message: message, + Trace: trace, + Level_ID: levelId, + PluginName: convertPluginName, + ObjectGUID: (objectGuid == null || objectGuid === undefined ? '00000000-0000-0000-0000-000000000000' : objectGuid) + }); + const newLogEntry = await this.EventLogView.findOne( { where: { ID: entry.ID }, plain: true } ); + if(levelId > -1) { // if not levelId = -1 | test-message + this.socketManager.broadcast('admin', 'eventlog_table', newLogEntry); + } + + if(this.app.locals.configuration.debug.active && levelId >= this.app.locals.configuration.debug.levelId) { + this.socketManager.broadcast('admin', 'eventlog', { levelId: levelId, pluginName: pluginName, datetime: `[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}]`, trace: `[${trace}]`, message: message }) + } + + console.log(`[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}][${convertPluginName}][${convertLevel}][${trace}]:`, message.replaceAll('
', '\r\n')); + } + + + /** + * Only creates a new log entry in EventLog database table, without sending socketEvent + * @param {string} [objectGuid] - MSSQL uniqueidentifier (UUID) or null = '00000000-0000-0000-0000-000000000000' AS 'SYSTEM' + * @param {number} levelId - -1=test, 0=success, 1=log, 2=warn, 4=error, 8=throw_exception + * @param {number} pluginId - ID of plugin + * @param {Object} socketSending - Use socket to send error message to admin web frontends. Object: { active: true, levelId: 1 } + * @param {Array} args - Message: string or comma seperated array of string + * args[0].stack !== undefined - => Client side (error) stack and message + */ + async writeLog(objectGuid, levelId, pluginName = null, ...args) { + const err = new Error(); + const stackLine = args[0].stack !== undefined ? args[0].stack : err.stack.split('\n')[2]; // calls trace-line + const trace = args[0].stack !== undefined ? stackLine.split("\n")[1].trim() : stackLine.match(/\/.*\d+/)[0].replace(this.app.locals.path.root, ''); // path:line:column + // const message = !Array.isArray(...args) ? [...args][0] : [...args][0][0] + const message = args[0].stack !== undefined ? args[0].message : args.join('\r\n\t'); + + const convertLevel = levelId == -1 ? 'test' : levelId == 0 ? 'success' : levelId == 1 ? 'log' : levelId == 2 ? 'warn' : levelId == 4 ? 'error' : 'throw exception'; + const convertPluginName = pluginName == null ? 'SYSTEM' : pluginName; + + const entry = await this.EventLog.create({ + Message: message, + Trace: trace, + Level_ID: levelId, + PluginName: convertPluginName, + ObjectGUID: (objectGuid == null || objectGuid === undefined ? '00000000-0000-0000-0000-000000000000' : objectGuid) + }); + + const newLogEntry = await this.EventLogView.findOne( { where: { ID: entry.ID }, plain: true } ); + if(levelId > -1) { // if not levelId = -1 | test-message + this.socketManager.broadcast('admin', 'eventlog_table', newLogEntry); + } + + console.log(`[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}][${convertPluginName}][${convertLevel}][${trace}]:`, message.replaceAll('
', '\r\n')); + } + + + /** + * Clears the eventlog database table + */ + async clear() { + const err = new Error(); + const stackLine = err.stack.split('\n')[2]; // calls trace-line + const trace = stackLine.match(/\/.*\d+/)[0].replace(this.app.locals.path.root, ''); // path:line:column + + const message = `${this.EventLog.tableName} cleared successfully`; + await this.EventLog.destroy({ + where: {}, + truncate: true, + }); + this.socketManager.broadcast('admin', 'eventlog', { levelId: 0, pluginName: 'SYSTEM', datetime: `[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}]`, trace: `[${trace}]`, message: message }) + console.log(`[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}][${trace}]`, message.replaceAll('
', '\r\n')); + } + + /** + * Get all eventlogs from database table + */ + async getAllEventLogs() { + try { + const logs = await this.EventLogView.findAll({ order: [['ID', 'DESC']] }); // Alle Zeilen abrufen + const logsArray = logs.map(log => log.get({ plain: true })); // Sequelize-Objekte in reine JS-Objekte + + return logsArray; // Rückgabe als Array + } catch (error) { + console.error('Fehler beim Abrufen der EventLogs:', error); + return []; + } + } + + + /** + * Get a specified range of eventlogs from database table + */ + async getEventLogs(fromID = null, toID) { + try { + const whereStatement = fromID != null ? { + where: { + ID: { + [Op.gte]: fromID, // größer gleich vonID + [Op.lte]: toID // kleiner gleich bisID + } + }, + order: [['ID', 'DESC']] + } : + { + limit: toID, + order: [['ID', 'DESC']] + } + + + const logs = await this.EventLogView.findAll(whereStatement); + + const logsArray = logs.map(log => log.get({ plain: true })); + return logsArray; + } catch (error) { + console.error('Fehler beim Abrufen der EventLogs:', error); + return []; + } + } +} + +module.exports = EventManager; \ No newline at end of file diff --git a/src/services/fileSystemManager.js b/src/services/fileSystemManager.js new file mode 100644 index 0000000..0d8197a --- /dev/null +++ b/src/services/fileSystemManager.js @@ -0,0 +1,187 @@ +const fs = require('fs'); +const path = require('path'); + +class FileSystemManager { + /** + * @param {string} jsonPath - Pfad zur JSON-Datei + * @param {boolean} watch - ob die JSON-Datei überwacht werden soll (Live-Update) + */ + constructor() { } + + loadAllFiles(path, fileextension = null) { + let files = {}; + fs.readdirSync(path).forEach(file => { + if (fileextension == null || file.endsWith(fileextension)) { + const filefound = require(`${path}/${file}`); + files = { ...files, ...filefound }; // zusammenführen + } + }); + return files; + } + + getAllFiles(path, fileextension = null) { + let files = {}; + fs.readdirSync(path).forEach(file => { + if (fileextension == null || file.endsWith(fileextension)) { + const filefound = `${path}/${file}`; + files = { ...files, ...filefound }; // zusammenführen + } + }); + return files; + } + + +/** + * Liest rekursiv Dateien und gibt nur die gewünschten Attribute zurück. + * + * @param {string} dirPath - Startverzeichnis + * @param {string[]} attributes - gewünschte Attribute: + * ['name','fullPath','size','lastModified','isDirectory','extension', ...] + * @param {string|null} sortBy - Attribut zum Sortieren (z.B. 'lastModified' oder 'name') + * @param {string} order - 'asc' oder 'desc' + */ + readFiles(dirPath, attributes = ['name', 'fullPath'], sortBy = null, order = 'asc') { + let results = []; + + const items = fs.readdirSync(dirPath); + + for (const item of items) { + const fullPath = path.join(dirPath, item); + const stats = fs.statSync(fullPath); + const isDir = stats.isDirectory(); + + // Objekt mit ALLEN möglichen Infos + const allInfo = { + name: item, + nameWithoutExt: item.substring(0, item.indexOf('.')), + fullPath: fullPath, + size: stats.size, + lastModified: stats.mtime, + created: stats.birthtime, + isDirectory: isDir, + extension: isDir ? null : path.extname(item) + }; + + if (isDir) { + // rekursiv weitermachen + results = results.concat(this.readFiles(fullPath, attributes, null, order)); + } else { + // nur gewünschte Attribute ausgeben + const filtered = {}; + for (const attr of attributes) { + if (allInfo[attr] !== undefined) { + filtered[attr] = allInfo[attr]; + } + } + results.push(filtered); + } + } + + // Sortieren, falls gewünscht + if (sortBy) { + results.sort((a, b) => { + if (a[sortBy] < b[sortBy]) return order === 'asc' ? -1 : 1; + if (a[sortBy] > b[sortBy]) return order === 'asc' ? 1 : -1; + return 0; + }); + } + return results; +} + + + + + +/** + * Sammelt verschiedene Pattern-Ergebnisse aus Dateien mehrerer Ordner. + * + * @param {Object} options + * @param {string|string[]} options.folderPaths - Pfad oder Array von Pfaden zu Ordnern + * @param {string} [options.extension='.js'] - Dateiendung + * @param {Array<{ name: string, pattern: RegExp, mapFn?: Function }>} options.patterns - Liste von Pattern-Definitionen + * @param {boolean} [options.recursive=true] - Unterordner durchsuchen? + * @returns {Array} - kombinierte Ergebnisse aller Pattern + */ +collectFromFiles({ + folderPaths, + extension = '.js', + patterns, + recursive = true +}) { + const results = []; + + // if only one path selected + const paths = Array.isArray(folderPaths) ? folderPaths : [folderPaths]; + + function readDir(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory() && recursive) { + readDir(fullPath); + } else if (entry.isFile() && entry.name.endsWith(extension)) { + const content = fs.readFileSync(fullPath, 'utf8'); + + // 👉 NEU: fallback wenn keine patterns + if (!patterns || patterns.length === 0) { + results.push({ + file: entry.name, + fullPath + }); + continue; + } + + for (const { name, pattern, mapFn } of patterns) { + let match; + while ((match = pattern.exec(content)) !== null) { + const mapped = mapFn + ? mapFn(match, entry.name, fullPath, name) + : { file: entry.name, type: name, match: match[0] }; + results.push(mapped); + } + } + } + } + } + + // Run through multiple paths + for (const dir of paths) { + if (fs.existsSync(dir)) { + readDir(path.resolve(dir)); + } else { + console.warn(`Ordner nicht gefunden: ${dir}`); + } + } + + return results; +} + + + + // JSON-Datei laden + loadJSON(path) { + try { + const rawData = fs.readFileSync(path, 'utf8'); + return JSON.parse(rawData); + } catch (err) { + console.log(err) + return err + } + } + + + // Check file-/path + exists(path) { + try { + const info = fs.statSync(path); + if (!info.isFile() && !info.isDirectory()) return { status: false, levelId: 4, message: `${info.isFile() ? 'Datei' : 'Pfad'} ${path} existiert nicht` }; + return { status: true, levelId: 0, message: `${info.isFile() ? 'Datei' : 'Pfad'} existiert: ${path}` }; + } catch (err) { + return ; + } + } +} + +module.exports = FileSystemManager; \ No newline at end of file diff --git a/src/services/hotReload.js b/src/services/hotReload.js new file mode 100644 index 0000000..a5e99e2 --- /dev/null +++ b/src/services/hotReload.js @@ -0,0 +1,377 @@ +// hotReloadJson.js +const fs = require('fs'); +const path = require('path'); +const { watch } = require('chokidar'); + +module.exports = { + File: class { + constructor(filePath, options = {}) { + if (!filePath || typeof filePath !== 'string') { + return { levelId: 4, pluginName: null, message: 'filePath muss ein gültiger String sein' }; + } + + this.filePath = path.resolve(filePath); + this.fileType = options.fileType || "json"; // "json" oder "js" + this.historyLimit = options.historyLimit || 50; + this.autoCreate = options.autoCreate || false; + this.autoSaveJs = options.autoSaveJs || false; // neu: JS-Dateien optional speichern + this.callbacks = []; + this.history = []; + this.redoStack = []; + + if (!fs.existsSync(this.filePath)) { + if(this.autoCreate) { + fs.writeFileSync(this.filePath, this.fileType === 'json' ? '{ }' : 'module.exports = {}'); + } else { + return { levelId: 4, pluginName: null, message: `Datei existiert nicht: ${this.filePath}` }; + } + } + + const initialData = this.loadFile(); + if (!initialData) { + this.data = {}; + return { levelId: 4, pluginName: null, message: `Fehler beim Laden der Datei: ${this.filePath}` }; + } else { + this.data = initialData; + } + + this.proxy = this.createProxy(this.data); + this.pushHistory(null, this.deepCopy(this.data)); + + this.watcher = watch(this.filePath, { ignoreInitial: true }); + this.watcher.on('change', () => this.reload()); + } + + loadJS() { + try { + delete require.cache[require.resolve(this.filePath)]; + const module = require(this.filePath); + if (typeof module !== "object") { + console.warn("[LiveJSON] JS-Datei exportiert kein Objekt:", this.filePath); + return {}; + } + return this.deepCopy(module); + } catch (err) { + console.error(`[LiveJSON] Fehler beim Laden der JS-Datei ${this.filePath}:`, err.message); + return null; + } + } + + loadJSON() { + try { + const raw = fs.readFileSync(this.filePath, 'utf-8'); + return JSON.parse(raw); + } catch (err) { + console.error(`[LiveJSON] Fehler beim Laden von ${this.filePath}:`, err.message); + return null; + } + } + + loadFile() { + if (this.fileType === "js") return this.loadJS(); + return this.loadJSON(); + } + + saveToFile() { + try { + if (this.fileType === 'json') { + fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), 'utf-8'); + } else if (this.fileType === 'js' && this.autoSaveJs) { + const content = 'module.exports = ' + JSON.stringify(this.data, null, 2); + fs.writeFileSync(this.filePath, content, 'utf-8'); + } + return { levelId: 0, pluginName: null, message: 'Änderungen erfolgreich gespeichert' }; + } catch (err) { + return { levelId: 4, pluginName: null, message: `Fehler beim Schreiben in Datei: ${err.message}` }; + } + } + + createProxy(obj) { + const self = this; + return new Proxy(obj, { + get(target, prop) { + const value = target[prop]; + if (value && typeof value === 'object') { + return self.createProxy(value); + } + return value; + }, + set(target, prop, value) { + const oldValue = self.deepCopy(target); + const result = Reflect.set(target, prop, value); + const newValue = self.deepCopy(target); + self.pushHistory(oldValue, newValue); + self.saveToFile(); + self.triggerCallbacks(oldValue, newValue); + return result; + }, + deleteProperty(target, prop) { + const oldValue = self.deepCopy(target); + const result = Reflect.deleteProperty(target, prop); + const newValue = self.deepCopy(target); + self.pushHistory(oldValue, newValue); + self.saveToFile(); + self.triggerCallbacks(oldValue, newValue); + return result; + } + }); + } + + reload() { + setTimeout(() => { + const oldData = this.deepCopy(this.data); + const newData = this.loadFile(); + if (newData) { + this.mergeObjects(this.data, newData); + this.pushHistory(oldData, this.deepCopy(this.data)); + // Trigger Callback jetzt mit absPath + this.triggerCallbacks(oldData, this.data, this.filePath); + } + }, 100); + return { levelId: 0, message: 'Reload initiiert' }; + } + + deepCopy(obj) { + return JSON.parse(JSON.stringify(obj)); + } + + pushHistory(oldState, newState) { + const entry = { + timestamp: new Date().toISOString(), + oldState, + newState + }; + this.history.push(entry); + if (this.history.length > this.historyLimit) this.history.shift(); + this.redoStack = []; + } + + undo() { + if (this.history.length < 2) return { levelId: 4, message: 'Nichts zum Rückgängigmachen' }; + const last = this.history.pop(); + this.redoStack.push(last); + const prev = this.history[this.history.length - 1]; + this.mergeObjects(this.data, prev.newState, true); + this.saveToFile(); + this.triggerCallbacks(last.newState, prev.newState); + return { levelId: 0, message: 'Undo erfolgreich', state: prev.newState }; + } + + redo() { + if (this.redoStack.length === 0) return { levelId: 4, message: 'Nichts zum Wiederherstellen' }; + const next = this.redoStack.pop(); + this.history.push(next); + this.mergeObjects(this.data, next.newState, true); + this.saveToFile(); + this.triggerCallbacks(next.oldState, next.newState); + return { levelId: 0, message: 'Redo erfolgreich', state: next.newState }; + } + + // Rekursive Diff inkl. Arrays + diff(oldObj, newObj) { + if (Array.isArray(oldObj) && Array.isArray(newObj)) return this.diffArray(oldObj, newObj); + if (this.isObject(oldObj) && this.isObject(newObj)) { + const changes = {}; + const allKeys = new Set([...Object.keys(oldObj || {}), ...Object.keys(newObj || {})]); + allKeys.forEach(key => { + const oldVal = oldObj[key]; + const newVal = newObj[key]; + if (this.isObject(oldVal) && this.isObject(newVal)) { + const childDiff = this.diff(oldVal, newVal); + if (Object.keys(childDiff).length > 0) changes[key] = childDiff; + } else if (Array.isArray(oldVal) && Array.isArray(newVal)) { + const arrayDiff = this.diffArray(oldVal, newVal); + if (arrayDiff.length > 0) changes[key] = arrayDiff; + } else if (oldVal !== newVal) { + changes[key] = { old: oldVal, new: newVal }; + } + }); + return changes; + } + return oldObj !== newObj ? { old: oldObj, new: newObj } : {}; + } + + // Diff für Arrays auf Elementebene + diffArray(oldArr, newArr) { + const maxLen = Math.max(oldArr.length, newArr.length); + const changes = []; + for (let i = 0; i < maxLen; i++) { + if (i >= oldArr.length) { + changes.push({ index: i, old: undefined, new: newArr[i] }); + } else if (i >= newArr.length) { + changes.push({ index: i, old: oldArr[i], new: undefined }); + } else if (JSON.stringify(oldArr[i]) !== JSON.stringify(newArr[i])) { + changes.push({ index: i, old: oldArr[i], new: newArr[i] }); + } + } + return changes; + } + + isObject(value) { + return value && typeof value === 'object' && !Array.isArray(value); + } + + // Merge für Objekte und Arrays + mergeObjects(target, source, removeExtra = false) { + if (Array.isArray(target) && Array.isArray(source)) { + target.length = 0; + source.forEach((el) => target.push(this.deepCopy(el))); + return; + } + + Object.keys(target).forEach(key => { + if (source[key] === undefined && removeExtra) delete target[key]; + }); + + Object.keys(source).forEach(key => { + if (this.isObject(source[key])) { + if (!target[key] || !this.isObject(target[key])) target[key] = {}; + this.mergeObjects(target[key], source[key], removeExtra); + } else if (Array.isArray(source[key])) { + target[key] = []; + this.mergeObjects(target[key], source[key], removeExtra); + } else { + target[key] = source[key]; + } + }); + } + + onChange(cb) { + if (typeof cb === 'function') this.callbacks.push(cb); + } + + triggerCallbacks(oldState, newState, absPath = null) { + const delta = this.diff(oldState, newState); + this.callbacks.forEach(cb => cb({ oldState, newState, delta, path: absPath })); + } + + get live() { + return this.proxy; + } + + close() { + this.watcher.close(); + return { levelId: 0, pluginName: null, message: 'Watcher gestoppt' }; + } + }, + + Folder: class { + constructor(folderPath, options = {}) { + if (!folderPath || typeof folderPath !== 'string') { + return { + levelId: 4, + pluginName: null, + message: "folderPath muss ein gültiger String sein" + }; + } + + this.folderPath = path.resolve(folderPath); + this.files = {}; // relPath -> File-Instanz + this.liveProxies = {}; // relPath -> Proxy für live Zugriff + + this.options = { + ignoreInitial: false, + persistent: true, + ignored: options.ignored ?? null, + }; + + // Callbacks + this.handlers = { + onAdd: options.onAdd || (() => {}), + onChange: options.onChange || (() => {}), + onUnlink: options.onUnlink || (() => {}), + onAddDir: options.onAddDir || (() => {}), + onUnlinkDir: options.onUnlinkDir || (() => {}), + onReady: options.onReady || (() => {}), + onError: options.onError || (() => {}), + }; + + try { + this.watcher = watch(this.folderPath, this.options); + this.bindEvents(); + return { levelId: 0, pluginName: null, message: "SmartFolderWatcher mit History erfolgreich initialisiert" }; + } catch (err) { + return { levelId: 4, pluginName: null, message: `Watcher konnte nicht gestartet werden: ${err.message}` }; + } + } + + // Datei initial laden / als File-Instanz tracken + loadTrackedFile(absPath) { + const rel = path.relative(this.folderPath, absPath); + const ext = path.extname(absPath).toLowerCase(); + const fileType = ext === '.js' ? 'js' : 'json'; + + // File-Instanz erstellen, falls nicht existiert + if (!this.files[rel]) { + const { File } = module.exports; + const fileInstance = new File(absPath, { fileType, autoCreate: true, autoSaveJs: true }); + this.files[rel] = fileInstance; + this.liveProxies[rel] = fileInstance.live; + } else { + // reload falls schon bekannt + this.files[rel].reload(); + } + + return this.files[rel]; + } + + // Chokidar Events + bindEvents() { + this.watcher + .on("add", (file) => { + const rel = path.relative(this.folderPath, file); + const fileInstance = this.loadTrackedFile(file); + this.handlers.onAdd(rel, fileInstance.data, file); // rel, Inhalt, absPath + }) + .on("change", (file) => { + const rel = path.relative(this.folderPath, file); + const fileInstance = this.loadTrackedFile(file); + this.handlers.onChange(rel, fileInstance.data, file); + }) + .on("unlink", (file) => { + const rel = path.relative(this.folderPath, file); + delete this.liveProxies[rel]; + delete this.files[rel]; + this.handlers.onUnlink(rel, file); + }) + .on("addDir", (dir) => { + const rel = path.relative(this.folderPath, dir); + this.handlers.onAddDir(rel, dir); + }) + .on("unlinkDir", (dir) => { + const rel = path.relative(this.folderPath, dir); + this.handlers.onUnlinkDir(rel, dir); + }) + .on("ready", () => { + this.handlers.onReady(this.liveProxies); + }) + .on("error", (err) => { + this.handlers.onError(err); + }); + } + + // Zugriff auf alle live Proxies + get live() { + return this.liveProxies; + } + + // Undo / Redo auf Dateiebene + undoFile(relPath) { + if (!this.files[relPath]) return { levelId: 4, message: 'Datei nicht gefunden' }; + return this.files[relPath].undo(); + } + + redoFile(relPath) { + if (!this.files[relPath]) return { levelId: 4, message: 'Datei nicht gefunden' }; + return this.files[relPath].redo(); + } + + close() { + this.watcher.close(); + // close alle File-Watcher + Object.values(this.files).forEach(f => f.close()); + return { levelId: 0, pluginName: null, message: "FolderWatcher gestoppt" }; + } +} + +}; diff --git a/src/services/identityManager.js b/src/services/identityManager.js new file mode 100644 index 0000000..be4d7b4 --- /dev/null +++ b/src/services/identityManager.js @@ -0,0 +1,302 @@ +const { Op } = require('sequelize'); + +class IdentityManager { + constructor(adManager, AuthenticationModel) { + this.ad = adManager || null; + this.Authentication = AuthenticationModel; + } + + /** + * ----------------------------------------------------- + * REQUIRED FIELDS (MANUAL USER) + * ----------------------------------------------------- + */ + REQUIRED_FIELDS = [ + 'sAMAccountName', + 'mail', + 'givenName', + 'sn', + 'password' + ]; + + /** + * ----------------------------------------------------- + * VALIDATE MANUAL USER + * ----------------------------------------------------- + */ + validateManualUser(user) { + const missing = []; + + for (const field of this.REQUIRED_FIELDS) { + if ( + user[field] === undefined || + user[field] === null || + user[field] === '' + ) { + missing.push(field); + } + } + + if (missing.length) { + throw new Error( + `Fehlende Pflichtfelder: ${missing.join(', ')}` + ); + } + + // 🔍 Optional: einfache Zusatzvalidierungen + if (user.mail && !user.mail.includes('@')) { + throw new Error('Ungültige E-Mail-Adresse'); + } + + if (user.password && user.password.length < 6) { + throw new Error('Passwort muss mindestens 6 Zeichen lang sein'); + } + } + + /** + * ----------------------------------------------------- + * FIXED MAPPING (AD → Authentication) + * ----------------------------------------------------- + */ + mapAdObject(obj) { + if (!obj || !obj.objectGUID) return null; + + return { + ObjectGUID: obj.objectGUID, + sAMAccountName: obj.sAMAccountName || obj.cn || null, + mail: obj.mail || null, + givenName: obj.givenName || null, + sn: obj.sn || null, + employeeID: obj.employeeID || null, + title: obj.title || null, + department: obj.department || null, + streetAddress: obj.streetAddress || null, + userAccountControl_ID: obj.userAccountControl || null, + authenticationType_ID: 1, + telephoneNumber: obj.telephoneNumber || null, + physicalDeliveryOfficeName: obj.physicalDeliveryOfficeName || null, + distinguishedName: obj.dn || null, + password: null, + refreshtoken: null, + active: true, + online: false + }; + } + + /** + * ----------------------------------------------------- + * DEDUP (wie SQL UNION) + * ----------------------------------------------------- + */ + deduplicateByGUID(items) { + const map = new Map(); + + for (const item of items) { + if (!item?.ObjectGUID) continue; + map.set(item.ObjectGUID, item); + } + + return Array.from(map.values()); + } + + /** + * ----------------------------------------------------- + * TABLE CHECK / CREATE + * ----------------------------------------------------- + */ + async ensureTable() { + const qi = this.Authentication.sequelize.getQueryInterface(); + const tables = await qi.showAllTables(); + + const exists = tables.includes('Authentication'); + + if (!exists) { + await this.Authentication.sync(); + return false; + } + + return true; + } + + /** + * ----------------------------------------------------- + * CORE SYNC (INTELLIGENT) + * ----------------------------------------------------- + */ + async syncFromAD() { + if (!this.ad) { + throw new Error('AD nicht konfiguriert'); + } + + const [users, groups, computers] = await Promise.all([ + this.ad.findUsers('*'), + this.ad.findGroups('*'), + this.ad.getComputers() + ]); + + const mapped = this.deduplicateByGUID([ + ...users.map(u => this.mapAdObject(u)), + ...groups.map(g => this.mapAdObject(g)), + ...computers.map(c => this.mapAdObject(c)) + ].filter(Boolean)); + + if (!mapped.length) { + return { total: 0, deactivated: 0 }; + } + + await this.Authentication.bulkCreate(mapped, { + updateOnDuplicate: [ + 'mail', + 'givenName', + 'sn', + 'employeeID', + 'title', + 'department', + 'streetAddress', + 'userAccountControl_ID', + 'telephoneNumber', + 'physicalDeliveryOfficeName', + 'distinguishedName', + 'active' + ] + }); + + const existing = await this.Authentication.findAll({ + where: { authenticationType_ID: 1 }, + attributes: ['ObjectGUID'] + }); + + const adGuids = new Set(mapped.map(u => u.ObjectGUID)); + + const toDeactivate = existing + .filter(e => !adGuids.has(e.ObjectGUID)) + .map(e => e.ObjectGUID); + + if (toDeactivate.length) { + await this.Authentication.update( + { active: false }, + { + where: { + ObjectGUID: toDeactivate + } + } + ); + } + + return { + total: mapped.length, + deactivated: toDeactivate.length, + adGuids: Array.from(adGuids) + }; + } + + /** + * ----------------------------------------------------- + * OPTIONAL: HARD DELETE + * ----------------------------------------------------- + */ + async removeDeletedADObjects(adGuids) { + return this.Authentication.destroy({ + where: { + authenticationType_ID: 1, + ObjectGUID: { + [Op.notIn]: adGuids + } + } + }); + } + + /** + * ----------------------------------------------------- + * RECREATE + * ----------------------------------------------------- + */ + async recreateAuthentications(hardReset = false) { + let message = ''; + + const exists = await this.ensureTable(); + + if (!exists) { + message = 'Tabelle wurde neu erstellt '; + } + + try { + const result = await this.syncFromAD(); + + message += `Sync abgeschlossen (${result.total} Objekte)`; + + if (result.deactivated) { + message += `, ${result.deactivated} deaktiviert`; + } + + if (hardReset) { + const deleted = await this.removeDeletedADObjects(result.adGuids); + message += `, ${deleted} gelöscht`; + } + + } catch (err) { + message += 'Fehler: ' + err.message; + } + + return message; + } + + /** + * ----------------------------------------------------- + * MANUAL USER (MIT VALIDATION) + * ----------------------------------------------------- + */ + async createManualUser(user) { + this.validateManualUser(user); + + return this.Authentication.create({ + ...user, + authenticationType_ID: 2, + active: true, + online: false + }); + } + + /** + * ----------------------------------------------------- + * MANUAL BULK (MIT VALIDATION) + * ----------------------------------------------------- + */ + async createManualUsers(users) { + const errors = []; + + users.forEach((user, index) => { + try { + this.validateManualUser(user); + } catch (err) { + errors.push(`User ${index}: ${err.message}`); + } + }); + + if (errors.length) { + throw new Error(errors.join(' | ')); + } + + return this.Authentication.bulkCreate( + users.map(user => ({ + ...user, + authenticationType_ID: 2, + active: true, + online: false + })) + ); + } + + /** + * ----------------------------------------------------- + * GET USER + * ----------------------------------------------------- + */ + async getUser(username) { + return this.Authentication.findOne({ + where: { sAMAccountName: username } + }); + } +} + +module.exports = IdentityManager; \ No newline at end of file diff --git a/src/services/notifyTrayManager.js b/src/services/notifyTrayManager.js new file mode 100644 index 0000000..7eee9c2 --- /dev/null +++ b/src/services/notifyTrayManager.js @@ -0,0 +1,137 @@ +class notifyTrayManager { + /** + * @param {object} models - { UserNotificationObjects, UserNotifications } + */ + constructor(notifyTrayModel, notifyTrayView, notifyTrayObject) { + this.objects = notifyTrayObject; + this.view = notifyTrayView; + this.notifications = notifyTrayModel; + } + + //-------------------------------------------- + // 1. Notification Object erstellen oder updaten + //-------------------------------------------- + async upsertObject({ id = null, pluginName, message, json, actionRequired = false, expiresAt = null }) { + if (id) { + // ID existiert → upsert + return this.objects.upsert({ + ID: id, + PluginName: pluginName, + Message: message, + JSON: json ? JSON.stringify(json) : null, + ActionRequired: actionRequired, + CreatedAt: new Date(), + ExpiresAt: expiresAt + }); + } else { + // ID null → neue Zeile einfügen, Auto-Increment nutzen + const obj = await this.objects.create({ + PluginName: pluginName, + Message: message, + JSON: json ? JSON.stringify(json) : null, + ActionRequired: actionRequired, + CreatedAt: new Date(), + ExpiresAt: expiresAt + }); + + // zurückgeben inklusive der generierten ID + return obj; + } + } + + //-------------------------------------------- + // 2. User benachrichtigen (insert) + //-------------------------------------------- + async notifyUsers({ objectId, objectGuids = [] }) { + + if (!objectGuids.length) return; + const notificationsToCreate = objectGuids.map(guid => ({ + ObjectGUID: guid, + NotifyTrayObject_ID: objectId, + })); + return this.notifications.bulkCreate(notificationsToCreate); + } + + //-------------------------------------------- + // 3. Offene Notifications abfragen + //-------------------------------------------- + async getOpenNotifications(objectGuid) { + return await this.view.findAll({ + where: { + ObjectGUID: objectGuid, + SeenAt: null + }, + // include: [{ + // model: this.objects, + // as: 'NotificationObject', + // required: true, // join zwingend + // where: { + // [this.objects.sequelize.Op.Or]: [ + // { ExpiresAt: null }, + // { ExpiresAt: { [this.objects.sequelize.Op.gt]: new Date() } } + // ] + // } + // }], + order: [[ 'SeenAt', 'ASC']] + }); + } + + //-------------------------------------------- + // 4. Einzelne Notification als gesehen markieren + //-------------------------------------------- + async markAsSeen(objectGuid, notificationId, value) { + return this.notifications.update( + { SeenAt: value ? new Date() : null }, + { where: { ID: notificationId, ObjectGUID: objectGuid } } + ); + } + + //-------------------------------------------- + // 5. Alle Notifications eines Users als gesehen markieren + //-------------------------------------------- + async markAllAsSeen(objectGuid, value) { + return this.notifications.update( + { SeenAt: value ? new Date() : null }, + { where: { ObjectGUID: objectGuid, SeenAt: null } } + ); + } + + //-------------------------------------------- + // 6. Komplett-Flow: Object upserten + User benachrichtigen + //-------------------------------------------- + async createAndNotify({ objectId = null, pluginName, json, actionRequired, message, objectGuids, expiresAt }) { + if(objectGuids === null || !objectGuids.length) { + throw 'There is no user to notify for tray'; + } + let obj; + if (objectId) { + obj = await this.upsertObject({ + id: objectId, + pluginName, + message, + json, + actionRequired, + expiresAt: Date.now() + }); + } else { + obj = await this.upsertObject({ + id: null, + pluginName, + message, + json, + actionRequired, + expiresAt: Date.now() + }); + } + + // Auto-Increment ID korrekt für Notifications nutzen + const finalObjectId = obj.ID || obj.id; // Sequelize gibt je nach DB dialect mal 'ID' oder 'id' + + return await this.notifyUsers({ + objectId: finalObjectId, + objectGuids + }); + } +} + +module.exports = notifyTrayManager; \ No newline at end of file diff --git a/src/services/pluginManager.js b/src/services/pluginManager.js new file mode 100644 index 0000000..69197bd --- /dev/null +++ b/src/services/pluginManager.js @@ -0,0 +1,497 @@ +const fs = require('fs'); +const path = require('path'); +const fse = require('fs-extra'); +const expressHandlebars = require('express-handlebars'); +const { execSync } = require('child_process'); +const { File: HotReload } = require(`@services/hotReload.js`); + +class PluginManager { + constructor(app, pluginModel, pluginBasePath, filePermissions, service) { + this.app = app; + this.Plugin = pluginModel; + this.pluginBasePath = pluginBasePath; + this.filePermissions = filePermissions; + this.service = service; + + this.plugins = new Map(); + + // FIX: metadata jetzt Map (aber Struktur bleibt gleich) + this.metadata = new Map(); + + this.hbsInstance = expressHandlebars.create({ + extname: '.hbs', + defaultLayout: 'main', + }); + + if(!fs.existsSync(this.pluginBasePath)) { + fs.mkdirSync(this.pluginBasePath) + } + } + + async loadAll() { + if (!fs.existsSync(this.pluginBasePath)) { + return [{ levelId: 4, message: `Plugin-Pfad existiert nicht: ${this.pluginBasePath}` }]; + } + + const pluginDirs = fs.readdirSync(this.pluginBasePath) + .filter(f => fs.lstatSync(path.join(this.pluginBasePath, f)).isDirectory()); + + const loadPromises = pluginDirs.map(dir => this.load(dir)); + return await Promise.all(loadPromises); + } + + async load(name, withActivate = false) { + const pluginPath = path.join(this.pluginBasePath, name); + const metadataPath = path.join(pluginPath, 'plugin.json'); + + const Plugin = require(pluginPath); + const instance = new Plugin(pluginPath, this.app, this.service); + + let result; + + try { + // FIX: HotReload pro Plugin korrekt speichern + const meta = new HotReload(metadataPath, { + historyLimit: 50, + autoCreate: true + }); + + this.metadata.set(name, meta); + + if (Object.keys(meta.live).length === 0) { + meta.live = this.__pluginTemplate(name); + meta.saveToFile(); + this.__setPermissions(pluginPath); + } + + // Routes + const routesPath = path.join(pluginPath, 'routes.js'); + if (fs.existsSync(routesPath)) { + const routes = require(routesPath); + routes(this.app, pluginPath, meta.live, this.service); + } + + // Sockets + const socketsPath = path.join(pluginPath, 'sockets.js'); + if (fs.existsSync(socketsPath)) { + const sockets = require(socketsPath); + sockets(this.app, pluginPath, meta.live, this.service); + } + + this.app.use(`/${name}`, require('express').static(path.join(pluginPath, 'public'))); + + const viewsDir = path.join(pluginPath, 'views'); + + this.app.set("views", [ + ...(Array.isArray(this.app.get("views")) ? this.app.get("views") : [this.app.get("views")]), + path.join(this.pluginBasePath) + ]); + + this.registerPartialsRecursive(viewsDir, `plugins/${name}`); + + await this.Plugin.upsert({ + Name: name, + Active: withActivate ? true : (await this.Plugin.findOne({ where: { Name: meta.live.name }})).Active, + Version: meta.live.version + }); + + const static_meta = this.service.get('fileSystemManager').loadJSON(metadataPath); + + this.plugins.set(name, { + ...static_meta, + pluginPath, + viewPath: viewsDir, + metadataPath + }); + + meta.live.active = withActivate ? true : (await this.Plugin.findOne({ where: { Name: meta.live.name }})).Active; + + result = { + status: 'load', + pluginName: name, + metadata: meta.live, + levelId: 0, + message: `Plugin ${name} geladen` + }; + + return result; + + } catch (err) { + console.log(err) + return { + status: 'load', + pluginName: name, + levelId: 4, + message: [err.message, err] + }; + } + } + + async unload(name) { + let result; + const pluginPath = path.join(this.pluginBasePath, name); + const metadataPath = path.join(pluginPath, 'plugin.json'); + + try { + if (!this.plugins.has(name)) { + return { + status: 'unload', + pluginName: name, + levelId: 4, + message: 'Plugin nicht vorhanden' + }; + } + + // FIX: korrektes metadata handling + const meta = this.metadata.get(name); + + if (meta) { + meta.live.active = false; + meta.saveToFile(); + } + + await this.Plugin.update({ Active: false }, { where: { Name: name } }); + + result = { + status: 'unload', + pluginName: name, + levelId: 0, + message: `Plugin ${name} entladen` + }; + + return result; + + } catch (err) { + return { + status: 'unload', + pluginName: name, + levelId: 4, + message: [err.message, err] + }; + } + } + + async reload(name) { + const unloadResult = await this.unload(name); + if (unloadResult.levelId === 4) return unloadResult; + + delete require.cache[require.resolve(path.join(this.pluginBasePath, name))]; + + return await this.load(name); + } + + async delete(name) { + const pluginPath = path.join(this.pluginBasePath, name); + + try { + await this.unload(name); + + this.plugins.delete(name); + await this.Plugin.destroy({ where: { Name: name } }); + + if (fs.existsSync(pluginPath)) { + await fse.remove(pluginPath); + } + + return { + status: 'delete', + pluginName: name, + levelId: 0, + message: `Plugin ${name} gelöscht` + }; + + } catch (err) { + return { + status: 'delete', + pluginName: name, + levelId: 4, + message: [err.message, err] + }; + } + } + + async create(name, options = {}) { + const pluginPath = path.join(this.pluginBasePath, name); + + if (fs.existsSync(pluginPath)) { + return { levelId: 4, pluginName: name, message: `Plugin existiert bereits` }; + } + + try { + const folders = [ + 'views', + 'views/children', + 'public/javascript', + 'public/styles', + 'public/helpers', + 'public/images', + 'public/others', + 'docs' + ]; + + for (const folder of folders) { + await fse.ensureDir(path.join(pluginPath, folder)); + } + + const files = { + 'plugin.json': this.__pluginTemplate(name), + + 'index.js': +`module.exports = class Plugin { + constructor(pluginPath, app, service) { + this.pluginPath = pluginPath; + this.app = app; + this.service = service; + } +}`, + + 'routes.js': +`module.exports = async (app, pluginPath, metadata, service) => {};`, + + 'sockets.js': +`module.exports = (app, socketManager, pluginPath, metadata, eventManager) => {};`, + + 'docs/help.html': +`

Hilfedatei für ${name}

${options.description || 'Beschreibung hier einfügen'}

`, + + 'views/index.hbs': +`
{{plugin.name}}
` + }; + + for (const [file, content] of Object.entries(files)) { + await fse.outputFile(path.join(pluginPath, file), content); + } + + this.__setPermissions(pluginPath); + + await this.Plugin.upsert({ + Name: name, + Active: false, + Version: options.version || '1.0.0' + }); + + return { + status: 'create', + levelId: 0, + pluginName: name, + message: `Plugin ${name} erstellt` + }; + + } catch (err) { + return { + status: 'create', + levelId: 4, + pluginName: name, + message: err.message + }; + } + } + + __pluginTemplate(name, options = { }) { + return { + name, + description: options.description || 'Beschreibung hier einfügen', + version: options.version || `1.${new Date().getFullYear().toString().slice(-2)}.${new Date().getMonth() + 1}.${new Date().getDate()}`, + menu: { + label: name, + items:[ + { + label: name, + view: "index", + defaultSize: { width: '800px', height: '600px' }, + icon: "../../images/app.png", + permissions: ["*"] + } + ] + }, + config: options.config || {}, + active: true + } + } + + __setPermissions(pluginPath) { + if (!this.filePermissions.user && !this.filePermissions.group) return; + + try { + let uid, gid; + + if (this.filePermissions.user) { + uid = parseInt(execSync(`id -u ${this.filePermissions.user}`).toString().trim()); + } + + if (this.filePermissions.group) { + gid = parseInt(execSync(`getent group ${this.filePermissions.group} | cut -d: -f3`).toString().trim()); + } + + const apply = dir => { + const entries = fs.readdirSync(dir); + + for (const entry of entries) { + const full = path.join(dir, entry); + const stat = fs.lstatSync(full); + + fs.chownSync(full, uid, gid); + + if (stat.isDirectory()) apply(full); + } + }; + + apply(pluginPath); + + } catch (err) { + throw new Error(err.message); + } + } + + update(name, updates = {}) { + const meta = this.metadata.get(name); + + if (!meta) { + return { status: 'update', levelId: 4, message: 'Plugin nicht geladen' }; + } + + Object.assign(meta.live, updates); + + meta.saveToFile?.(); + return { + status: 'update', + pluginName: name, + levelId: 0, + metadata: meta.live, + message: `Plugin aktualisiert ${Object.keys(updates)[0]}: ${Object.values(updates)[0]}` + }; + } + + async rename(oldName, newName) { + const oldPath = path.join(this.pluginBasePath, oldName); + const newPath = path.join(this.pluginBasePath, newName); + const newMetadataPath = path.join(newPath, 'plugin.json'); + const routesPath = path.join(newPath, 'routes.js'); + + const updateRoutesFile = async (filePath, oldName, newName) => { + let content = await fse.readFile(filePath, 'utf-8'); + + // ersetzt /window/OLDNAME/... → /window/NEWNAME/... + const regex = new RegExp(`(app\\.(get|post)\\(['"\`]\\/window\\/)${oldName}(\\/[^'"\`]*['"\`])`, 'g'); + + content = content.replace(regex, `$1${newName}$3`); + + await fse.writeFile(filePath, content, 'utf-8'); + } + + + try { + // 🔹 Checks + if (!this.plugins.has(oldName)) { + return { + status: 'rename', + levelId: 4, + message: 'Plugin nicht gefunden' + }; + } + + if (fs.existsSync(newPath)) { + return { + status: 'rename', + levelId: 4, + message: 'Neuer Name existiert bereits' + }; + } + + // 🔹 1. copy + await fse.copy(oldPath, newPath); + + // 🔹 2. plugin.json fix + const metaRaw = this.service.get('fileSystemManager').loadJSON(newMetadataPath); + metaRaw.name = newName; + metaRaw.menu.label = newName; + await fse.writeJSON(newMetadataPath, metaRaw, { spaces: 2 }); + + // 🔹 3. routes fix (falls vorhanden) + if (fs.existsSync(routesPath)) { + await updateRoutesFile(routesPath, oldName, newName); + } + + // 🔥 4. ALTES Plugin vorher unloaden + await this.unload(oldName); + + // 🔹 5. metadata cleanup + this.metadata.delete(oldName); + + // 🔹 6. DB update + await this.Plugin.update( + { Name: newName }, + { where: { Name: oldName } } + ); + + // 🔥 7. kurzer delay → verhindert ENOENT durch watcher + await new Promise(res => setTimeout(res, 100)); + + // 🔹 8. neues Plugin laden + await this.load(newName, metaRaw.active); + + // 🔹 9. alten Ordner löschen + await fse.remove(oldPath); + + // 🔹 10. altes Plugin löschen + this.plugins.delete(oldName); + + return { + status: 'rename', + pluginName: newName, + levelId: 0, + message: `Plugin ${oldName} → ${newName} umbenannt` + }; + + } catch (err) { + // 🔹 Rollback: neuen Ordner entfernen + if (fs.existsSync(newPath)) { + await fse.remove(newPath); + } + + return { + status: 'rename', + pluginName: oldName, + levelId: 4, + message: [err.message, err] + }; + } + } + + getStatus() { + return Array.from(this.plugins.entries()).map(([name, plugin]) => { + const meta = this.metadata.get(name); + + return { + name, + ...plugin, + ...(meta ? meta.live : {}) + }; + }); + } + + compileHbsTemplate(template) { + return this.hbsInstance.handlebars.compile(template); + } + + registerPartialsRecursive(dir, prefix = '') { + if (!fs.existsSync(dir)) return; + + const files = fs.readdirSync(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = fs.lstatSync(fullPath); + + if (stat.isDirectory()) { + this.registerPartialsRecursive(fullPath, path.join(prefix, file)); + } else if (file.endsWith('.hbs')) { + const name = path.join(prefix, path.basename(file, '.hbs')).replace(/\\/g, '/'); + const template = fs.readFileSync(fullPath, 'utf8'); + this.hbsInstance.handlebars.registerPartial(name, template); + } + } + } +} + +module.exports = PluginManager; \ No newline at end of file diff --git a/src/services/renderWindow.js b/src/services/renderWindow.js new file mode 100644 index 0000000..6949b30 --- /dev/null +++ b/src/services/renderWindow.js @@ -0,0 +1,85 @@ +const { service } = require('@root/server.js'); + +module.exports = { + renderWindow: async function(app, view, data = {}, extraData = {}, res) { + const name = res.req.body.name; + const label = res.req.body.viewLabel; + try { + const plugin = app.locals.startMenuItems.find(plugin => plugin.name == name); + const windowData = plugin.menu.items.find(item => item.label == label); + + // Alle Daten zusammenführen + const templateData = { + ...{ appname: plugin.menu.label, label: label, section: plugin.section }, + ...windowData, + // ...data, + ...extraData + }; + // console.log(templateData) + + // Zuerst Plugin-View rendern + app.render(view, templateData, (err, contentHtml) => { + if (err) { + service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, name, err); + return res.status(500).send(err.message); + } + // Dann Window-Partial rendern + app.render('partials/window', { + layout: false, + contentHtml, + ...templateData + }, (err, html) => { + if (err) { + service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, name, err); + return res.status(500).send(err.message); + } + res.status(200).send(html); + }); + }); + + } catch (err) { + service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, name, err); + res.status(500).send(err.message); + } + }, + renderView: async function(app, view, data = {}, res) { + const payload = res.req.body; + + try { + + // console.log(app.locals.startMenuItems.find(name == ).menu.items.find(item => )) + // Alle Daten zusammenführen + const templateData = { + ...payload, + ...{ appname: payload.name, section: 'view' }, + ...{ name: payload.name, view: payload.view, viewLabel: payload.viewLabel}, + ...data, + }; + console.log(templateData) + // Zuerst Plugin-View rendern + app.render(view, templateData, (err, contentHtml) => { + + if (err) { + service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, payload.name, err); + return res.status(500).send(err.message); + } + // Dann Window-Partial rendern + app.render('partials/child', { + layout: false, + contentHtml, + ...templateData + }, (err, html) => { + if (err) { + service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, payload.name, err); + return res.status(500).send(err.message); + } + res.status(200).send(html); + }); + }); + + } catch (err) { + service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, payload.name, err); + res.status(500).send(err.message); + } + } +}; \ No newline at end of file diff --git a/src/services/socketManager.js b/src/services/socketManager.js new file mode 100644 index 0000000..0612fc7 --- /dev/null +++ b/src/services/socketManager.js @@ -0,0 +1,336 @@ +class SocketManager { + constructor(io) { + this.io = io; + + this.namespaces = new Map(); + this.clients = new Map(); + + this.io.on('connection', socket => {}); + } + + + add(namespace, exists) { + if (this.namespaces.has(namespace)) return this.namespaces.get(namespace); + + const nsp = this.io.of(namespace); + const clients = new Map(); + this.clients.set(namespace, clients); + + nsp.on('connection', socket => { + const { objectGuid, sAMAccountName } = socket.handshake.auth || {}; + if (!objectGuid) return; + + clients.set(objectGuid, { + socket, + userName: sAMAccountName + }); + + // console.log(`${sAMAccountName} [${objectGuid}] connected to ${namespace}`); + + socket.on('disconnect', () => { + clients.delete(objectGuid); + // console.log(`${sAMAccountName} [${objectGuid}] disconnected from ${namespace}`); + }); + }); + + this.namespaces.set(namespace, nsp); + return nsp; + } + + + async addAsync(namespace, exists) { + return new Promise(resolve => { + this.add(namespace, exists) + + // simulierter async Init (z. B. DB) + setTimeout(resolve, 0); + }); + } + + + /** + * Broadcast in namespace + * @param {string} namespace - Namespace-Name + * @param {string} event - Event-Name + * @param {any} data - Event-Data + */ + broadcast(namespace, event, data) { + const nsp = this.namespaces.get(namespace); + if (nsp) nsp.emit(event, data); + } + + + /** + * Event-Handler für einen Namespace registrieren + * @param {string} namespace - Namespace-Name + * @param {string} event - Event-Name + * @param {Function} callback - Callback mit (socket, data) + */ + // on(namespace, event, callback) { + // const nsp = this.namespaces.get(namespace); + // if (!nsp) { + // console.warn(`Namespace ${namespace} doesn't exist`); + // return; + // } + + // nsp.on('connection', (socket) => { + // socket.on(event, (data) => { + // // übergibt socket als Kontext, damit sendTo automatisch weiß, wer anfragte + // callback(socket, data, (responseEvent, responseData, targetGuid = null) => { + // this.sendTo(namespace, targetGuid, responseEvent, responseData, socket); + // }); + // }); + // }); + // } + on(namespace, event, callback) { + const nsp = this.namespaces.get(namespace); + if (!nsp) { + console.warn(`Namespace ${namespace} doesn't exist`); + return; + } + + nsp.on('connection', (socket) => { + socket.on(event, (data) => callback(socket, data)); + }); + } + + + registerClient(namespace, socket) { + const nsp = this.namespaces.get(namespace); + if (!nsp) { + console.warn(`Namespace ${namespace} does not exist`); + return; + } + + const auth = socket.handshake.auth; + const objectGuid = auth.objectGuid; + const sAMAccountName = auth.sAMAccountName; + + if (!objectGuid) { + console.warn('Socket has no objectGuid, cannot register'); + return; + } + + socket.customId = objectGuid; + + // In Map speichern + this.clients.set(objectGuid, { + userName: sAMAccountName, + socketId: socket.id, + socket: socket + }); + + console.log(`${sAMAccountName} [${objectGuid}] registered to namespace ${namespace}`); + + // Disconnect-Handler + socket.on('disconnect', () => { + this.clients.delete(objectGuid); + console.log(`${sAMAccountName} [${objectGuid}] disconnected from namespace ${namespace}`); + }); + }; + + + /** + * Entfernt einen Client aus dem Namespace + * @param {string} objectGuid - GUID des Clients + * @param {string} [namespace] - optional, um Namespace-spezifisch zu loggen + */ + unregisterClient(objectGuid) { + try { + const client = this.clients.get(objectGuid); + if (!client) return; + + // Optional: Socket trennen + client.socket.disconnect(); + + this.clients.delete(objectGuid); + const name = client.userName; + console.log(`${name} [${objectGuid}] manuell entfernt`); + } catch (err) { + console.log(err); + } + } + + + /** + * Send a personal message to ObjectGUID + * @param {string} namespace - Namespace-Name + * @param {string} objectGuid - Socket-ID + * @param {string} event - Event-Name + * @param {any} data - Event-Data + * @param {object} [senderSocket] - optionaler Absender-Socket + */ + sendTo(namespace, objectGuid, event, data, senderSocket = null) { + const clients = this.clients.get(namespace); + if (!clients) return; + + // Antwort an Absender + if (!objectGuid && senderSocket) { + senderSocket.emit(event, data); + return; + } + + const client = clients.get(objectGuid); + if (!client?.socket) return; + + client.socket.emit(event, data); + } +} + +module.exports = SocketManager; + +// const WebSocket = require("ws"); +// const url = require("url"); + +// class SocketManager { +// constructor() { +// this.namespaces = new Map(); // "/admin" → { clients:Set, handlers:Map } +// this.clients = new Map(); // objectGuid → { userName, socket } + +// // Default-Namespaces wie bei Socket.IO +// this.add("/"); +// this.add("/admin"); +// } + + +// /** +// * Namespace erstellen +// */ +// add(namespace) { +// if (this.namespaces.has(namespace)) return this.namespaces.get(namespace); + +// const ns = { +// clients: new Set(), +// handlers: new Map() // event → callback +// }; + +// this.namespaces.set(namespace, ns); +// return ns; +// } + + +// /** +// * Upgrade (HTTP → WebSocket) pro Namespace auswerten +// */ +// handleUpgrade(namespace, req, socket, head) { +// if (!this.namespaces.has(namespace)) { +// socket.destroy(); +// return; +// } + +// const ns = this.namespaces.get(namespace); + +// const wss = new WebSocket.Server({ noServer: true }); + +// wss.handleUpgrade(req, socket, head, ws => { +// this._onConnection(namespace, ws, req); +// }); +// } + + +// /** +// * Verbindung herstellen + authentifizieren +// */ +// _onConnection(namespace, socket, req) { +// const params = new URLSearchParams(url.parse(req.url).query); +// const objectGuid = params.get("objectGuid"); +// const sAMAccountName = params.get("sAMAccountName") || "Unknown"; + +// socket.namespace = namespace; +// socket.objectGuid = objectGuid; +// socket.userName = sAMAccountName; + +// // Helper zum JSON-Senden +// socket.sendJSON = (obj) => socket.send(JSON.stringify(obj)); + +// const ns = this.namespaces.get(namespace); +// ns.clients.add(socket); + +// if (objectGuid) { +// this.clients.set(objectGuid, { userName: sAMAccountName, socket: socket }); +// console.log(`${sAMAccountName} [${objectGuid}] connected to [${namespace}]`); +// } + +// // Nachrichten-Handling +// socket.on("message", raw => { +// let msg; +// try { msg = JSON.parse(raw); } +// catch { return; } + +// const event = msg.event; +// const data = msg.data; + +// const cb = ns.handlers.get(event); +// if (cb) cb(socket, data); +// }); + +// // Disconnect +// socket.on("close", () => { +// ns.clients.delete(socket); + +// if (objectGuid) { +// this.clients.delete(objectGuid); +// console.log(`${sAMAccountName} [${objectGuid}] disconnected from namespace ${namespace}`); +// } +// }); +// } + + +// /** +// * Events registrieren wie socket.on("event") +// */ +// on(namespace, event, callback) { +// const ns = this.namespaces.get(namespace); +// if (!ns) return console.warn(`Namespace ${namespace} doesn't exist`); +// ns.handlers.set(event, callback); +// } + + +// /** +// * Broadcast in Namespace +// */ +// broadcast(namespace, event, data) { +// const ns = this.namespaces.get(namespace); +// if (!ns) return; + +// for (const client of ns.clients) { +// if (client.readyState === WebSocket.OPEN) { +// client.sendJSON({ event, data }); +// } +// } +// } + + +// /** +// * Persönliche Nachricht an GUID +// */ +// sendTo(namespace, objectGuid, event, data, sender = null) { +// if (!objectGuid && sender) { +// sender.sendJSON({ event, data }); +// return; +// } + +// const client = this.clients.get(objectGuid); +// if (!client) return; + +// if (client.socket.readyState === WebSocket.OPEN) { +// client.socket.sendJSON({ event, data }); +// } +// } + + +// /** +// * Client manuell entfernen +// */ +// unregisterClient(objectGuid) { +// const client = this.clients.get(objectGuid); +// if (!client) return; + +// client.socket.close(4000, "manual kick"); +// this.clients.delete(objectGuid); + +// console.log(`${client.userName} [${objectGuid}] manually removed`); +// } +// } + +// module.exports = SocketManager; diff --git a/src/services/sqlManager.js b/src/services/sqlManager.js new file mode 100644 index 0000000..323523f --- /dev/null +++ b/src/services/sqlManager.js @@ -0,0 +1,139 @@ +const Sequelize = require('sequelize'); + +class SqlManager { + constructor() { + if (SqlManager._instance) return SqlManager._instance; + this.instances = {}; + SqlManager._instance = this; + } + + /** + * Neue MSSQL Instanz registrieren + */ + addInstance(name, config) { + if (this.instances[name]) { + console.log(`[INFO] Instance "${name}" already exists.`); + return this.instances[name]; + } + + const sequelize = new Sequelize(config.database, config.user, config.password, { + host: config.host, + dialect: 'mssql', + port: config.port || 1433, + logging: config.logging || false, + pool: { + max: 10, + min: 0, + acquire: 30000, + idle: 10000 + }, + dialectOptions: { + options: { + encrypt: false, + trustServerCertificate: true + } + } + }); + + this.instances[name] = { sequelize, config }; + return sequelize; + } + + /** + * Instanz abrufen + */ + getInstance(name) { + const inst = this.instances[name]; + if (!inst) { + throw new Error(`Instance "${name}" not found.`); + } + return inst.sequelize; + } + + /** + * Reconnect-Logik (falls Verbindung verloren geht) + */ + async reconnect(name) { + console.log(`[WARN] Reconnecting to ${name}...`); + + const old = this.instances[name]; + if (!old) throw new Error(`Instance "${name}" not found`); + + const { config } = old; + + // alte Verbindung killen + try { await old.sequelize.close(); } catch {} + + // neu verbinden + const newSequelize = new Sequelize( + config.database, + config.username, + config.password, + { + ...old.sequelize.options + } + ); + + this.instances[name].sequelize = newSequelize; + + try { + await newSequelize.authenticate(); + console.log(`[OK] Reconnected to ${name}`); + return true; + } catch (err) { + console.log(`[ERROR] Reconnect to ${name} failed:`, err); + return false; + } + } + + /** + * Query ausführen (async) + */ + async query(instanceName, sql, options) { + const sequelize = this.getInstance(instanceName); + + try { + const [result] = await sequelize.query(sql, options); + return result; + } catch (err) { + console.error(`[ERROR] Query failed on "${instanceName}":`, err.message); + + if (err.original && err.original.code === "ECONNCLOSED") { + await this.reconnect(instanceName); + return this.query(instanceName, sql, options); + } + + throw err; + } + } + + /** + * Pseudo-synchrones Query (Wirkung wie .querySync, intern aber async) + * Du wartest darauf, also fühlt es sich synchron an. + */ + querySync(instanceName, sql, options) { + return this.query(instanceName, sql, options); + } + + /** + * Verbindung testen + */ + async test(instanceName) { + const sequelize = this.getInstance(instanceName); + + try { + await sequelize.authenticate(); + return { + levelId: 0, + message: `${sequelize.config.database} database connection hergestellt` + }; + } catch (error) { + return { + levelId: 0, + message: [`Unable to connect to ${sequelize.config.database}`, error] + }; + } + } +} + +module.exports = SqlManager; diff --git a/src/sockets/adminSocket.js b/src/sockets/adminSocket.js new file mode 100644 index 0000000..2702d0f --- /dev/null +++ b/src/sockets/adminSocket.js @@ -0,0 +1,14 @@ +module.exports = (app, socketManager, namespace, eventManager) => { + const adminSocket = socketManager.namespaces.get(namespace); + + // socketManager.on(namespace, 'plugin_status', (socket, data) => { + // eventManger.write(data.objectGuid, data.levelId, data.pluginName, app.locals.configuration.debug, data.message) + // }) + + socketManager.on(namespace, 'eventlog', (socket, data) => { + eventManager.write(data.objectGuid, data.levelId, data.pluginName, data.message) + }); + + socketManager.on(namespace, 'heartbeat', () => { + }) +} \ No newline at end of file diff --git a/src/sockets/mainSocket.js b/src/sockets/mainSocket.js new file mode 100644 index 0000000..5bd9423 --- /dev/null +++ b/src/sockets/mainSocket.js @@ -0,0 +1,25 @@ +const path = require('path'); +const fs = require('fs'); +const startMenuItemContext = require('@models/integratedStartmenuItems.js') + +module.exports = (app, socketManager, namespace, pluginManager, authenticationModel, fileSystemManager, eventManager, activeDirectory) => { + const mainSocket = socketManager.namespaces.get(namespace); + + socketManager.on(namespace, 'heartbeat', () => setInterval(() => console.log('test'), 1000)); + + socketManager.on(namespace, 'event', (socket, data) => { + socket.emit('event', { pluginName: data.pluginName, datetime: global.dateFormat(new Date(), 'dd.mm.yyyy HH:MM:SS'), levelId: data.levelId, message: data.message }); + // eventManager.write(data.objectGuid, data.levelId, data.pluginName, data.message) + }); + + // global.json.configuration.onChange(info => { + // console.log(info.delta) + // }); + + + mainSocket.on('connection', socket => { + socket.on('changePartial', ({ partial, data }) => { + socket.emit('updatePartial', { partial, data }); + }); + }) +} \ No newline at end of file diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..e57a3a0 --- /dev/null +++ b/utils.js @@ -0,0 +1,156 @@ +const { exec } = require('child_process'); +const path = require('path'); +const { permission } = require('process'); +const { dirname } = require('path'); +const { File: HotReload } = require(`@services/hotReload.js`); +const { service } = require(`@root/server.js`); +let integratedStartmenuItems = require('@models/integratedStartmenuItems'); + + +global.path = { + root: dirname(require.main.filename), + source:`${dirname(require.main.filename)}/src`, + public: `${dirname(require.main.filename)}/public`, + plugins: `${dirname(require.main.filename)}/plugins` +}; + + +global.json = { + releaseNotes: new HotReload(path.join(global.path.source, 'models', 'releasenotes.json')), + configuration: new HotReload(path.join(global.path.source, 'models', 'configuration.json')), + stylesheet: new HotReload(path.join(global.path.source, 'models', 'stylesheet.json')), + indexRoutes: new HotReload(path.join(global.path.source, 'routes', 'indexRoutes.js'), { historyLimit: 50, fileType: 'js' }), + startMenuItems: new HotReload(path.join(global.path.source, 'models', 'integratedStartmenuItems.js'), { historyLimit: 50, fileType: 'js' }) +} + + +module.exports = startMenuItems = async function(app, sAMAccountName) { +function safeClone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + delete integratedStartmenuItems; + + integratedStartmenuItems = safeClone(json.startMenuItems.live); + + const plugins = service + .get('pluginManager') + .getStatus() + .map(({ config, ...plugin }) => ({ + ...safeClone(plugin), + section: 'Plugin' + })); + + let getAllPlugins = [...plugins, ...integratedStartmenuItems]; + + for (const plugin of getAllPlugins) { + + plugin.menu.items = await Promise.all( + (plugin.menu.items || []).map(async item => { + + const authorized = + item.label === 'hr' || + item.permissions.includes('Administration') + ? global.json.configuration.live.administration.includes(sAMAccountName) + : item.permissions.includes('*') || + ( + await Promise.all( + item.permissions.map(async permission => + (await service.get('activeDirectoryManager').getGroup(permission)) && + (await service.get('activeDirectoryManager').isUserMemberOfRecursive(sAMAccountName, permission)) + ) + ) + ).some(Boolean); + + return { + ...safeClone(item), + authorized + }; + }) + ); + + plugin.onlyAdministration = + plugin.menu.items.every(item => !item.authorized) && + !global.json.configuration.live.administration.includes(sAMAccountName); + } + + getAllPlugins = getAllPlugins + .filter(plugin => !plugin.onlyAdministration) + .filter(plugin => plugin.active); + + app.locals.startMenuItems = getAllPlugins; + + return [...getAllPlugins]; +}; + + +/** + * Convert date into custom dateformat + * @param {any} date - Valid date as datetype or string + * @param {string} [format] - (optional) date characters are lowercase, time characters are uppercase; If is null, format will be "dd.mm.yyyy HH:MM:SS" +*/ +module.exports = dateFormat = function(date, format = null) { + format = (format == null ? "dd.mm.yyyy HH:MM:SS" : format); + const finish_date = new Date(date); + + return format + .replace('yyyy', finish_date.getFullYear()) + .replace('yy', finish_date.getFullYear().toString().slice(-2)) + .replace('mm', ("0" + (finish_date.getMonth() + 1)).slice(-2)) + .replace('dd', ("0" + finish_date.getDate()).slice(-2)) + .replace('HH', ("0" + finish_date.getHours()).slice(-2)) + .replace('MM', ("0" + finish_date.getMinutes()).slice(-2)) + .replace('SS', ("0" + finish_date.getSeconds()).slice(-2)); +} + + +/** + * Limits the number of functions that can be executed in parallel. + */ +// module.exports = createLimiter = function(max) { +// let active = 0; +// const queue = []; + +// const runNext = () => { +// if (active >= max || queue.length === 0) return; + +// active++; +// const { fn, resolve, reject } = queue.shift(); + +// fn() +// .then(resolve) +// .catch(reject) +// .finally(() => { +// active--; +// runNext(); +// }); +// }; + +// return fn => +// new Promise((resolve, reject) => { +// queue.push({ fn, resolve, reject }); +// runNext(); +// }); +// } + + + + +/** + * try to call an existing host by name or ip adress + * @param {string} hostname - Valid hostname or ip adress + * @param {string} [format] - (optional) date characters are lowercase, time characters are uppercase; If is null, format will be "dd.mm.yyyy HH:MM:SS" +*/ +module.exports = ping = function(hostname, cbErr, cb, ttl = 1) { + exec(`ping ${hostname} -c 1 -W ${ttl}`, function (err, stdout, stderr) { + if(err) { + cbErr(err); + } else { + cb(stdout); + } + }); +} + + +module.exports = isObject = function(param) { + return param !== null && typeof param === 'object' && !Array.isArray(param); +} \ No newline at end of file