diff --git a/.env.example b/.env.example index 0d9bf9d..55d9653 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -DB_TYPE = (mysql|sqlite|postgres) # default ist mysql +DB_TYPE = (mysql|postgres) # default ist mysql ## BSP für mysql DB_PORT = 3306 @@ -14,28 +14,10 @@ DB_NAME = database_name DB_USERNAME = database_username DB_PASSWORD = database_password -## BSP für sqlite -DB_HOST = filename.db - +## Dev only SERVER_PORT = portnumber -JWT_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 # besitzt default -JWT_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 15m -REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 1d -PWA_REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 5d - -MAIL_USERNAME = mail_username -MAIL_PASSWORD = mail_password -MAIL_HOST = mail_hoststring -MAIL_PORT = mail_portnumber # default ist 587 -MAIL_SECURE = (true|false) # true für port 465, false für anders gewählten port - -CLUB_NAME = clubname #default FF Admin -CLUB_WEBSITE = https://my-club-website-url #optional, muss aber mit http:// oder https:// beginnen - -BACKUP_INTERVAL = number of days (min 1) # default 1 -BACKUP_COPIES = number of parallel copies # default 7 -BACKUP_AUTO_RESTORE = (true|false) # default ist true +APPLICATION_SECRET = mysecret USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15m diff --git a/Dockerfile b/Dockerfile index 4846250..08f68ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,6 +37,7 @@ RUN mkdir -p /app/files ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser COPY --from=build /app/src/templates /app/src/templates +COPY --from=build /app/src/assets /app/src/assets COPY --from=build /app/dist /app/dist COPY --from=build /app/node_modules /app/node_modules COPY --from=build /app/package.json /app/package.json diff --git a/README.md b/README.md index 91f6037..4cc0e40 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Dieses Projekt, `ff-admin-server`, ist das Backend zur Verwaltung von Mitglieder Eine Demo zusammen mit der `ff-admin` finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de). +Das Handbuch zur Anwendung finden sie unter [https://ff-admin.de/ff-admin-handbook](https://ff-admin.de/ff-admin-handbook). + ## Installation Das Image exposed nur den Port 5000. Die Env-Variable SERVER_PORT kann nur im lokal ausführenden dev-Kontext verwendet werden. @@ -25,26 +27,13 @@ services: container_name: ff_member_administration_server restart: unless-stopped environment: - - DB_TYPE= # default ist auf mysql gesetzt + - DB_TYPE= # default ist auf mysql gesetzt - DB_HOST=ff-db - DB_PORT= # default ist auf 3306 gesetzt - DB_NAME=ffadmin - DB_USERNAME=administration_backend - DB_PASSWORD= - - JWT_SECRET= - - JWT_EXPIRATION= # default ist auf 15m gesetzt - - REFRESH_EXPIRATION= # default ist auf 1d gesetzt - - PWA_REFRESH_EXPIRATION= # default ist auf 5d gesetzt - - MAIL_USERNAME= - - MAIL_PASSWORD= - - MAIL_HOST= - - MAIL_PORT= # default ist auf 587 gesetzt - - MAIL_SECURE= # default ist auf false gesetzt - - CLUB_NAME= # default ist auf FF Admin gesetzt - - CLUB_WEBSITE= - - BACKUP_INTERVAL= # alle x Tage, sonst keine - - BACKUP_COPIES= # Anzahl parallel bestehender Backups - - BACKUP_AUTO_RESTORE= # default ist auf true gesetzt + - APPLICATION_SECRET= - USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true - SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15 - SECURITY_STRICT_LIMIT_REQUEST_COUNT = strict_request_count # default ist 15 @@ -91,8 +80,6 @@ networks: Die Verwendung von postgres wird aufgrund des Verhaltens bei Datenbank-Update-Fehlern empfohlen. -Die Verwendung von SQLite wird nur für die Entwicklung oder lokale Tests empfohlen. - Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten: ```sh diff --git a/package-lock.json b/package-lock.json index f038d4b..e07b212 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,88 +1,98 @@ { "name": "ff-admin-server", - "version": "1.4.0", + "version": "1.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ff-admin-server", - "version": "1.4.0", + "version": "1.7.3", "license": "AGPL-3.0-only", "dependencies": { "cors": "^2.8.5", - "dotenv": "^16.4.5", + "crypto": "^1.0.1", + "dotenv": "^17.2.0", "express": "^5.1.0", - "express-rate-limit": "^7.5.0", + "express-rate-limit": "^7.5.1", "express-validator": "^7.2.1", "handlebars": "^4.7.8", - "helmet": "^8.0.0", + "helmet": "^8.1.0", "ics": "^3.8.1", "ip": "^2.0.1", "jsonwebtoken": "^9.0.2", + "lodash.clonedeep": "^4.5.0", "lodash.uniqby": "^4.7.0", "moment": "^2.30.1", "morgan": "^1.10.0", "ms": "^2.1.3", - "multer": "^1.4.5-lts.1", - "mysql": "^2.18.1", + "multer": "^2.0.1", "node-schedule": "^2.1.1", - "nodemailer": "^6.10.1", + "nodemailer": "^7.0.5", "pdf-lib": "^1.17.1", - "pg": "^8.13.1", - "puppeteer": "^24.6.1", + "pg": "^8.16.3", + "puppeteer": "^24.12.1", "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "rss-parser": "^3.13.0", - "socket.io": "^4.7.5", + "sharp": "^0.34.3", + "sharp-ico": "^0.1.5", + "socket.io": "^4.8.1", "speakeasy": "^2.0.0", - "sqlite3": "^5.1.7", - "typeorm": "^0.3.20", - "uuid": "^11.1.0" + "typeorm": "^0.3.25", + "uuid": "^11.1.0", + "validator": "^13.15.15" }, "devDependencies": { - "@types/cors": "^2.8.14", - "@types/express": "^5.0.1", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", "@types/ip": "^1.1.3", - "@types/jsonwebtoken": "^9.0.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/lodash.clonedeep": "^4.5.9", "@types/lodash.uniqby": "^4.7.9", - "@types/morgan": "^1.9.9", + "@types/morgan": "^1.9.10", "@types/ms": "^2.1.0", - "@types/multer": "^1.4.12", - "@types/mysql": "^2.15.21", - "@types/node": "^22.14.1", - "@types/node-schedule": "^2.1.6", - "@types/nodemailer": "^6.4.14", - "@types/pg": "~8.11.12", + "@types/multer": "^2.0.0", + "@types/node": "^24.0.13", + "@types/node-schedule": "^2.1.8", + "@types/nodemailer": "^6.4.17", + "@types/pg": "~8.15.4", "@types/qrcode": "~1.5.5", "@types/speakeasy": "^2.0.10", "@types/uuid": "^10.0.0", + "@types/validator": "^13.15.2", "ts-node": "10.9.2", "typescript": "^5.8.3" } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@canvas/image-data": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.0.0.tgz", + "integrity": "sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==", + "license": "MIT" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -96,12 +106,448 @@ "node": ">=12" } }, + "node_modules/@emnapi/runtime": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", + "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime/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==", + "license": "0BSD", + "optional": true + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "license": "MIT", - "optional": true + "optional": true, + "peer": true + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -233,6 +679,7 @@ "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" @@ -245,6 +692,7 @@ "deprecated": "This functionality has been moved to @npmcli/fs", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" @@ -259,6 +707,7 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", "optional": true, + "peer": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -295,16 +744,16 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.0.tgz", - "integrity": "sha512-HdHF4rny4JCvIcm7V1dpvpctIGqM3/Me255CB44vW7hDG1zYMmcBMjpNqZEDxdCfXGLkx5kP0+Jz5DUS+ukqtA==", + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", + "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.0", + "debug": "^4.4.1", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", - "semver": "^7.7.1", + "semver": "^7.7.2", "tar-fs": "^3.0.8", "yargs": "^17.7.2" }, @@ -333,6 +782,7 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 6" } @@ -393,18 +843,18 @@ } }, "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", - "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", "dependencies": { @@ -444,9 +894,9 @@ } }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", - "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "dev": true, "license": "MIT", "dependencies": { @@ -461,6 +911,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz", + "integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/lodash.uniqby": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/@types/lodash.uniqby/-/lodash.uniqby-4.7.9.tgz", @@ -479,9 +939,9 @@ "license": "MIT" }, "node_modules/@types/morgan": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", - "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", "dev": true, "license": "MIT", "dependencies": { @@ -496,38 +956,28 @@ "license": "MIT" }, "node_modules/@types/multer": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", - "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", "dev": true, "license": "MIT", "dependencies": { "@types/express": "*" } }, - "node_modules/@types/mysql": { - "version": "2.15.27", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", - "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "version": "24.0.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", + "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/node-schedule": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.7.tgz", - "integrity": "sha512-G7Z3R9H7r3TowoH6D2pkzUHPhcJrDF4Jz1JOQ80AX0K2DWTHoN9VC94XzFAPNMdbW9TBzMZ3LjpFi7RYdbxtXA==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.8.tgz", + "integrity": "sha512-k00g6Yj/oUg/CDC+MeLHUzu0+OFxWbIqrFfDiLi6OPKxTujvpv29mHGM8GtKr7B+9Vv92FcK/8mRqi1DK5f3hA==", "dev": true, "license": "MIT", "dependencies": { @@ -545,77 +995,15 @@ } }, "node_modules/@types/pg": { - "version": "8.11.12", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.12.tgz", - "integrity": "sha512-D8qPxnq0rgpvZPYwMxAZffxvlk2mtgimLC5kos8uM7+3wPKfTESxtpD49cfB5w1UnodZL7oYnjFHT5+cB3Gw9Q==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz", + "integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "pg-protocol": "*", - "pg-types": "^4.0.1" - } - }, - "node_modules/@types/pg/node_modules/pg-types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", - "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", - "dev": true, - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "pg-numeric": "1.0.2", - "postgres-array": "~3.0.1", - "postgres-bytea": "~3.0.0", - "postgres-date": "~2.1.0", - "postgres-interval": "^3.0.0", - "postgres-range": "^1.1.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@types/pg/node_modules/postgres-array": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", - "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/@types/pg/node_modules/postgres-bytea": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", - "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "obuf": "~1.1.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@types/pg/node_modules/postgres-date": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", - "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/@types/pg/node_modules/postgres-interval": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", - "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" + "pg-types": "^2.2.0" } }, "node_modules/@types/qrcode": { @@ -682,6 +1070,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/validator": { + "version": "13.15.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", + "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -697,7 +1092,8 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "license": "ISC", - "optional": true + "optional": true, + "peer": true }, "node_modules/accepts": { "version": "2.0.0", @@ -739,9 +1135,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", "engines": { "node": ">= 14" @@ -753,6 +1149,7 @@ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "humanize-ms": "^1.2.1" }, @@ -766,6 +1163,7 @@ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -827,7 +1225,8 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "license": "ISC", - "optional": true + "optional": true, + "peer": true }, "node_modules/are-we-there-yet": { "version": "3.0.1", @@ -836,6 +1235,7 @@ "deprecated": "This package is no longer supported.", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -844,21 +1244,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -903,16 +1288,16 @@ "license": "MIT" }, "node_modules/bare-events": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", - "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", + "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", "license": "Apache-2.0", "optional": true }, "node_modules/bare-fs": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.2.tgz", - "integrity": "sha512-8wSeOia5B7LwD4+h465y73KOdj5QHsbbuoUfPBi+pXgFJIPuG7SsiOdJuijWMyfid49eD+WivpfY7KT8gbAzBA==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz", + "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -1036,20 +1421,13 @@ "node": ">=10.0.0" } }, - "node_modules/bignumber.js": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", - "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "file-uri-to-path": "1.0.0" } @@ -1059,6 +1437,8 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -1084,25 +1464,13 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -1124,11 +1492,12 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1205,6 +1574,7 @@ "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "@npmcli/fs": "^1.0.0", "@npmcli/move-file": "^1.0.1", @@ -1235,6 +1605,7 @@ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -1248,6 +1619,7 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", "optional": true, + "peer": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -1307,14 +1679,16 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": ">=10" } }, "node_modules/chromium-bidi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-3.0.0.tgz", - "integrity": "sha512-ZOGRDAhBMX1uxL2Cm2TDuhImbrsEz5A/tTcVU6RpXEWaTNUNwsHW6njUXizh51Ir6iqHbKAfhA2XK33uBcLo5A==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", + "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", @@ -1330,6 +1704,7 @@ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -1348,6 +1723,19 @@ "node": ">=12" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1366,12 +1754,23 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "license": "ISC", "optional": true, + "peer": true, "bin": { "color-support": "bin.js" } @@ -1381,20 +1780,21 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", "engines": [ - "node >= 0.8" + "node >= 6.0" ], "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^2.2.2", + "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, @@ -1403,7 +1803,8 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "license": "ISC", - "optional": true + "optional": true, + "peer": true }, "node_modules/content-disposition": { "version": "1.0.0", @@ -1444,12 +1845,6 @@ "node": ">=6.6.0" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1522,6 +1917,13 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -1538,9 +1940,9 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1563,11 +1965,40 @@ "node": ">=0.10.0" } }, + "node_modules/decode-bmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/decode-bmp/-/decode-bmp-0.2.1.tgz", + "integrity": "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==", + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0", + "to-data-view": "^1.1.0" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/decode-ico": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-ico/-/decode-ico-0.4.1.tgz", + "integrity": "sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==", + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0", + "decode-bmp": "^0.2.0", + "to-data-view": "^1.1.0" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -1578,11 +2009,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=4.0.0" } @@ -1606,7 +2053,8 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/depd": { "version": "2.0.0", @@ -1618,18 +2066,18 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "engines": { "node": ">=8" } }, "node_modules/devtools-protocol": { - "version": "0.0.1425554", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1425554.tgz", - "integrity": "sha512-uRfxR6Nlzdzt0ihVIkV+sLztKgs7rgquY/Mhcv1YNCWDh5IZgl5mnn2aeEnW5stYTE0wwiF4RYVz8eMEpV1SEw==", + "version": "0.0.1464554", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", + "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", "license": "BSD-3-Clause" }, "node_modules/diff": { @@ -1649,9 +2097,9 @@ "license": "MIT" }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -1716,6 +2164,7 @@ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -1862,7 +2311,8 @@ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/error-ex": { "version": "1.3.2", @@ -1984,6 +2434,8 @@ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "license": "(MIT OR WTFPL)", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -2031,9 +2483,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", "license": "MIT", "engines": { "node": ">= 16" @@ -2042,7 +2494,7 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" + "express": ">= 4.11" } }, "node_modules/express-validator": { @@ -2058,6 +2510,15 @@ "node": ">= 8.0.0" } }, + "node_modules/express-validator/node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -2097,7 +2558,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/finalhandler": { "version": "2.1.0", @@ -2179,13 +2642,17 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -2198,7 +2665,8 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC", - "optional": true + "optional": true, + "peer": true }, "node_modules/function-bind": { "version": "1.1.2", @@ -2216,6 +2684,7 @@ "deprecated": "This package is no longer supported.", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", @@ -2292,9 +2761,9 @@ } }, "node_modules/get-uri": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", - "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", @@ -2309,7 +2778,9 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/glob": { "version": "7.2.3", @@ -2318,6 +2789,7 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2350,7 +2822,8 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC", - "optional": true + "optional": true, + "peer": true }, "node_modules/handlebars": { "version": "4.7.8", @@ -2390,7 +2863,8 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "license": "ISC", - "optional": true + "optional": true, + "peer": true }, "node_modules/hasown": { "version": "2.0.2", @@ -2418,7 +2892,8 @@ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "license": "BSD-2-Clause", - "optional": true + "optional": true, + "peer": true }, "node_modules/http-errors": { "version": "2.0.0", @@ -2468,10 +2943,17 @@ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ms": "^2.0.0" } }, + "node_modules/ico-endec": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ico-endec/-/ico-endec-0.1.6.tgz", + "integrity": "sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ==", + "license": "MPL-2.0" + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -2537,6 +3019,7 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.8.19" } @@ -2547,6 +3030,7 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -2556,7 +3040,8 @@ "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", "license": "ISC", - "optional": true + "optional": true, + "peer": true }, "node_modules/inflight": { "version": "1.0.6", @@ -2565,6 +3050,7 @@ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2580,7 +3066,9 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/ip": { "version": "2.0.1", @@ -2630,7 +3118,8 @@ "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/is-promise": { "version": "4.0.0", @@ -2638,12 +3127,6 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2762,6 +3245,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2847,6 +3336,7 @@ "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", @@ -2875,6 +3365,7 @@ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "debug": "4" }, @@ -2888,6 +3379,7 @@ "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@tootallnate/once": "1", "agent-base": "6", @@ -2903,6 +3395,7 @@ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -2917,6 +3410,7 @@ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -2930,6 +3424,7 @@ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -2940,6 +3435,7 @@ "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", @@ -3005,6 +3501,8 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -3018,6 +3516,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3039,6 +3538,8 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -3052,6 +3553,7 @@ "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -3065,6 +3567,7 @@ "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "minipass": "^3.1.0", "minipass-sized": "^1.0.3", @@ -3083,6 +3586,7 @@ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -3096,6 +3600,7 @@ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -3109,6 +3614,7 @@ "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -3121,6 +3627,8 @@ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -3151,7 +3659,9 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/moment": { "version": "2.30.1", @@ -3212,21 +3722,21 @@ "license": "MIT" }, "node_modules/multer": { - "version": "1.4.5-lts.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", - "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", + "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" + "type-is": "^1.6.18", + "xtend": "^4.0.2" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 10.16.0" } }, "node_modules/multer/node_modules/media-typer": { @@ -3272,42 +3782,6 @@ "node": ">= 0.6" } }, - "node_modules/mysql": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", - "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", - "license": "MIT", - "dependencies": { - "bignumber.js": "9.0.0", - "readable-stream": "2.3.7", - "safe-buffer": "5.1.2", - "sqlstring": "2.3.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mysql/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/mysql/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3330,7 +3804,9 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/negotiator": { "version": "1.0.0", @@ -3357,10 +3833,12 @@ } }, "node_modules/node-abi": { - "version": "3.74.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", - "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "semver": "^7.3.5" }, @@ -3372,7 +3850,9 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/node-gyp": { "version": "8.4.1", @@ -3380,6 +3860,7 @@ "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "env-paths": "^2.2.0", "glob": "^7.1.4", @@ -3414,9 +3895,9 @@ } }, "node_modules/nodemailer": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", - "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz", + "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -3428,6 +3909,7 @@ "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "abbrev": "1" }, @@ -3445,6 +3927,7 @@ "deprecated": "This package is no longer supported.", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", @@ -3476,13 +3959,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT" - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -3546,6 +4022,7 @@ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "aggregate-error": "^3.0.0" }, @@ -3663,6 +4140,7 @@ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3735,22 +4213,22 @@ "license": "MIT" }, "node_modules/pg": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", - "integrity": "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==", + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", "dependencies": { - "pg-connection-string": "^2.7.0", - "pg-pool": "^3.8.0", - "pg-protocol": "^1.8.0", - "pg-types": "^2.1.0", - "pgpass": "1.x" + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" }, "engines": { - "node": ">= 8.0.0" + "node": ">= 16.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.1.1" + "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -3762,16 +4240,16 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", - "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", "license": "MIT", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", - "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", "license": "MIT" }, "node_modules/pg-int8": { @@ -3783,29 +4261,19 @@ "node": ">=4.0.0" } }, - "node_modules/pg-numeric": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", - "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=4" - } - }, "node_modules/pg-pool": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", - "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", - "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", "license": "MIT" }, "node_modules/pg-types": { @@ -3887,18 +4355,13 @@ "node": ">=0.10.0" } }, - "node_modules/postgres-range": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", - "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", - "dev": true, - "license": "MIT" - }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -3924,27 +4387,17 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/prebuild-install/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/prebuild-install/node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -3957,6 +4410,8 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -3968,12 +4423,6 @@ "node": ">=6" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -3988,7 +4437,8 @@ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", "license": "ISC", - "optional": true + "optional": true, + "peer": true }, "node_modules/promise-retry": { "version": "2.0.1", @@ -3996,6 +4446,7 @@ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" @@ -4059,17 +4510,17 @@ } }, "node_modules/puppeteer": { - "version": "24.6.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.6.1.tgz", - "integrity": "sha512-/4ocGfu8LNvDbWUqJZV2VmwEWpbOdJa69y2Jivd213tV0ekAtUh/bgT1hhW63SDN/CtrEucOPwoomZ+9M+eBEg==", + "version": "24.12.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.12.1.tgz", + "integrity": "sha512-+vvwl+Xo4z5uXLLHG+XW8uXnUXQ62oY6KU6bEFZJvHWLutbmv5dw9A/jcMQ0fqpQdLydHmK0Uy7/9Ilj8ufwSQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.0", - "chromium-bidi": "3.0.0", + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1425554", - "puppeteer-core": "24.6.1", + "devtools-protocol": "0.0.1464554", + "puppeteer-core": "24.12.1", "typed-query-selector": "^2.12.0" }, "bin": { @@ -4080,17 +4531,17 @@ } }, "node_modules/puppeteer-core": { - "version": "24.6.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.6.1.tgz", - "integrity": "sha512-sMCxsY+OPWO2fecBrhIeCeJbWWXJ6UaN997sTid6whY0YT9XM0RnxEwLeUibluIS5/fRmuxe1efjb5RMBsky7g==", + "version": "24.12.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.12.1.tgz", + "integrity": "sha512-8odp6d3ERKBa3BAVaYWXn95UxQv3sxvP1reD+xZamaX6ed8nCykhwlOiHSaHR9t/MtmIB+rJmNencI6Zy4Gxvg==", "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.0", - "chromium-bidi": "3.0.0", - "debug": "^4.4.0", - "devtools-protocol": "0.0.1425554", + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1464554", "typed-query-selector": "^2.12.0", - "ws": "^8.18.1" + "ws": "^8.18.3" }, "engines": { "node": ">=18" @@ -4223,6 +4674,8 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "peer": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -4234,26 +4687,19 @@ } }, "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -4290,6 +4736,7 @@ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 4" } @@ -4301,6 +4748,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -4376,9 +4824,9 @@ "license": "ISC" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4449,6 +4897,59 @@ "sha.js": "bin.js" } }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "node_modules/sharp-ico": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/sharp-ico/-/sharp-ico-0.1.5.tgz", + "integrity": "sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q==", + "license": "MIT", + "dependencies": { + "decode-ico": "*", + "ico-endec": "*", + "sharp": "*" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4547,7 +5048,8 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC", - "optional": true + "optional": true, + "peer": true }, "node_modules/simple-concat": { "version": "1.0.1", @@ -4567,7 +5069,9 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/simple-get": { "version": "4.0.1", @@ -4588,12 +5092,29 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -4852,6 +5373,8 @@ "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", "hasInstallScript": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", @@ -4870,21 +5393,13 @@ } } }, - "node_modules/sqlstring": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", - "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "minipass": "^3.1.1" }, @@ -4910,9 +5425,9 @@ } }, "node_modules/streamx": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", - "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", "license": "MIT", "dependencies": { "fast-fifo": "^1.3.2", @@ -4996,6 +5511,8 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5005,6 +5522,8 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -5018,9 +5537,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", - "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", "license": "MIT", "dependencies": { "pump": "^3.0.0", @@ -5047,6 +5566,8 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -5056,6 +5577,8 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", + "optional": true, + "peer": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -5078,6 +5601,12 @@ "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", "license": "MIT" }, + "node_modules/to-data-view": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz", + "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -5148,6 +5677,8 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -5194,9 +5725,9 @@ "license": "MIT" }, "node_modules/typeorm": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.22.tgz", - "integrity": "sha512-P/Tsz3UpJ9+K0oryC0twK5PO27zejLYYwMsE8SISfZc1lVHX+ajigiOyWsKbuXpEFMjD9z7UjLzY3+ElVOMMDA==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.25.tgz", + "integrity": "sha512-fTKDFzWXKwAaBdEMU4k661seZewbNYET4r1J/z3Jwf+eAvlzMVpTLKAVcAzg75WwQk7GDmtsmkZ5MfkmXCiFWg==", "license": "MIT", "dependencies": { "@sqltools/formatter": "^1.2.5", @@ -5205,6 +5736,7 @@ "buffer": "^6.0.3", "dayjs": "^1.11.13", "debug": "^4.4.0", + "dedent": "^1.6.0", "dotenv": "^16.4.7", "glob": "^10.4.5", "sha.js": "^2.4.11", @@ -5299,14 +5831,26 @@ } }, "node_modules/typeorm/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/typeorm/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/typeorm/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -5385,9 +5929,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "license": "MIT" }, "node_modules/unique-filename": { @@ -5396,6 +5940,7 @@ "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "unique-slug": "^2.0.0" } @@ -5406,6 +5951,7 @@ "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "imurmurhash": "^0.1.4" } @@ -5446,9 +5992,9 @@ "license": "MIT" }, "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -5490,6 +6036,7 @@ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -5542,9 +6089,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -5606,7 +6153,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/yargs": { "version": "17.7.2", @@ -5668,9 +6217,9 @@ } }, "node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index bb0abcb..0500825 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "ff-admin-server", - "version": "1.4.0", + "version": "1.7.3", "description": "Feuerwehr/Verein Mitgliederverwaltung Server", "main": "dist/index.js", "scripts": { "start_ts": "ts-node src/index.ts", "typeorm": "typeorm-ts-node-commonjs", "migrate": "set DBMODE=migration && npx typeorm-ts-node-commonjs migration:generate ./src/migrations/%npm_config_name% -d ./src/data-source.ts", + "migrate-empty": "set DBMODE=migration && npx typeorm-ts-node-commonjs migration:create ./src/migrations/%npm_config_name%", "synchronize-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs schema:sync -d ./src/data-source.ts", "update-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts", "revert-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:revert -d ./src/data-source.ts", @@ -25,52 +26,56 @@ "license": "AGPL-3.0-only", "dependencies": { "cors": "^2.8.5", - "dotenv": "^16.4.5", + "crypto": "^1.0.1", + "dotenv": "^17.2.0", "express": "^5.1.0", - "express-rate-limit": "^7.5.0", + "express-rate-limit": "^7.5.1", "express-validator": "^7.2.1", "handlebars": "^4.7.8", - "helmet": "^8.0.0", + "helmet": "^8.1.0", "ics": "^3.8.1", "ip": "^2.0.1", "jsonwebtoken": "^9.0.2", + "lodash.clonedeep": "^4.5.0", "lodash.uniqby": "^4.7.0", "moment": "^2.30.1", "morgan": "^1.10.0", "ms": "^2.1.3", - "multer": "^1.4.5-lts.1", - "mysql": "^2.18.1", + "multer": "^2.0.1", "node-schedule": "^2.1.1", - "nodemailer": "^6.10.1", + "nodemailer": "^7.0.5", "pdf-lib": "^1.17.1", - "pg": "^8.13.1", - "puppeteer": "^24.6.1", + "pg": "^8.16.3", + "puppeteer": "^24.12.1", "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "rss-parser": "^3.13.0", - "socket.io": "^4.7.5", + "sharp": "^0.34.3", + "sharp-ico": "^0.1.5", + "socket.io": "^4.8.1", "speakeasy": "^2.0.0", - "sqlite3": "^5.1.7", - "typeorm": "^0.3.20", - "uuid": "^11.1.0" + "typeorm": "^0.3.25", + "uuid": "^11.1.0", + "validator": "^13.15.15" }, "devDependencies": { - "@types/cors": "^2.8.14", - "@types/express": "^5.0.1", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", "@types/ip": "^1.1.3", - "@types/jsonwebtoken": "^9.0.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/lodash.clonedeep": "^4.5.9", "@types/lodash.uniqby": "^4.7.9", - "@types/morgan": "^1.9.9", + "@types/morgan": "^1.9.10", "@types/ms": "^2.1.0", - "@types/multer": "^1.4.12", - "@types/mysql": "^2.15.21", - "@types/node": "^22.14.1", - "@types/node-schedule": "^2.1.6", - "@types/nodemailer": "^6.4.14", - "@types/pg": "~8.11.12", + "@types/multer": "^2.0.0", + "@types/node": "^24.0.13", + "@types/node-schedule": "^2.1.8", + "@types/nodemailer": "^6.4.17", + "@types/pg": "~8.15.4", "@types/qrcode": "~1.5.5", "@types/speakeasy": "^2.0.10", "@types/uuid": "^10.0.0", + "@types/validator": "^13.15.2", "ts-node": "10.9.2", "typescript": "^5.8.3" } diff --git a/src/assets/admin-logo.png b/src/assets/admin-logo.png new file mode 100644 index 0000000..a2760bc Binary files /dev/null and b/src/assets/admin-logo.png differ diff --git a/src/assets/icon.png b/src/assets/icon.png new file mode 100644 index 0000000..39a8174 Binary files /dev/null and b/src/assets/icon.png differ diff --git a/src/command/club/member/memberCommand.ts b/src/command/club/member/memberCommand.ts index f5c03d1..6d2767e 100644 --- a/src/command/club/member/memberCommand.ts +++ b/src/command/club/member/memberCommand.ts @@ -5,6 +5,7 @@ export interface CreateMemberCommand { nameaffix: string; birthdate: Date; internalId?: string; + note?: string; } export interface UpdateMemberCommand { @@ -15,6 +16,7 @@ export interface UpdateMemberCommand { nameaffix: string; birthdate: Date; internalId?: string; + note?: string; } export interface DeleteMemberCommand { diff --git a/src/command/club/member/memberCommandHandler.ts b/src/command/club/member/memberCommandHandler.ts index 78f1da0..128605d 100644 --- a/src/command/club/member/memberCommandHandler.ts +++ b/src/command/club/member/memberCommandHandler.ts @@ -23,6 +23,7 @@ export default abstract class MemberCommandHandler { nameaffix: createMember.nameaffix, birthdate: createMember.birthdate, internalId: createMember.internalId, + note: createMember.note, }) .execute() .then((result) => { @@ -49,6 +50,7 @@ export default abstract class MemberCommandHandler { nameaffix: updateMember.nameaffix, birthdate: updateMember.birthdate, internalId: updateMember.internalId, + note: updateMember.note, }) .where("id = :id", { id: updateMember.id }) .execute() diff --git a/src/command/club/member/memberEducationCommand.ts b/src/command/club/member/memberEducationCommand.ts new file mode 100644 index 0000000..be28106 --- /dev/null +++ b/src/command/club/member/memberEducationCommand.ts @@ -0,0 +1,23 @@ +export interface CreateMemberEducationCommand { + start: Date; + end?: Date; + note?: string; + place?: string; + memberId: string; + educationId: number; +} + +export interface UpdateMemberEducationCommand { + id: number; + start: Date; + end?: Date; + note?: string; + place?: string; + memberId: string; + educationId: number; +} + +export interface DeleteMemberEducationCommand { + id: number; + memberId: string; +} diff --git a/src/command/club/member/memberEducationCommandHandler.ts b/src/command/club/member/memberEducationCommandHandler.ts new file mode 100644 index 0000000..10f0311 --- /dev/null +++ b/src/command/club/member/memberEducationCommandHandler.ts @@ -0,0 +1,82 @@ +import { dataSource } from "../../../data-source"; +import { memberEducations } from "../../../entity/club/member/memberEducations"; +import DatabaseActionException from "../../../exceptions/databaseActionException"; +import InternalException from "../../../exceptions/internalException"; +import { + CreateMemberEducationCommand, + DeleteMemberEducationCommand, + UpdateMemberEducationCommand, +} from "./memberEducationCommand"; + +export default abstract class MemberEducationCommandHandler { + /** + * @description create memberEducation + * @param {CreateMemberEducationCommand} createMemberEducation + * @returns {Promise} + */ + static async create(createMemberEducation: CreateMemberEducationCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(memberEducations) + .values({ + note: createMemberEducation.note, + place: createMemberEducation.place, + start: createMemberEducation.start, + end: createMemberEducation.end, + memberId: createMemberEducation.memberId, + educationId: createMemberEducation.educationId, + }) + .execute() + .then((result) => { + return result.identifiers[0].id; + }) + .catch((err) => { + throw new DatabaseActionException("CREATE", "memberEducation", err); + }); + } + + /** + * @description update memberEducation + * @param {UpdateMemberEducationCommand} updateMemberEducation + * @returns {Promise} + */ + static async update(updateMemberEducation: UpdateMemberEducationCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(memberEducations) + .set({ + note: updateMemberEducation.note, + start: updateMemberEducation.start, + end: updateMemberEducation.end, + place: updateMemberEducation.place, + educationId: updateMemberEducation.educationId, + }) + .where("id = :id", { id: updateMemberEducation.id }) + .andWhere("memberId = :memberId", { memberId: updateMemberEducation.memberId }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("UPDATE", "memberEducation", err); + }); + } + + /** + * @description delete memberEducation + * @param {DeleteMemberEducationCommand} deleteMemberEducation + * @returns {Promise} + */ + static async delete(deleteMemberEducation: DeleteMemberEducationCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(memberEducations) + .where("id = :id", { id: deleteMemberEducation.id }) + .andWhere("memberId = :memberId", { memberId: deleteMemberEducation.memberId }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "memberEducation", err); + }); + } +} diff --git a/src/command/club/protocol/protocolAgendaCommandHandler.ts b/src/command/club/protocol/protocolAgendaCommandHandler.ts index 73ce349..190f288 100644 --- a/src/command/club/protocol/protocolAgendaCommandHandler.ts +++ b/src/command/club/protocol/protocolAgendaCommandHandler.ts @@ -34,18 +34,32 @@ export default abstract class ProtocolAgendaCommandHandler { /** * @description sync protocolAgenda + * @param {number} protocolId * @param {Array} syncProtocolAgenda * @returns {Promise} */ - static async sync(syncProtocolAgenda: Array): Promise { + static async sync(protocolId: number, syncProtocolAgenda: Array): Promise { + let currentAgenda = await ProtocolAgendaService.getAll(protocolId); return await dataSource .transaction(async (transactionalEntityManager) => { + let removed = currentAgenda.filter((ca) => !syncProtocolAgenda.some((spa) => spa.id == ca.id)); + for (const agenda of syncProtocolAgenda) { await transactionalEntityManager .createQueryBuilder() .update(protocolAgenda) .set(agenda) - .where({ id: agenda.id }) + .where({ id: agenda.id, protocolId }) + .execute(); + } + + if (removed.length != 0) { + await transactionalEntityManager + .createQueryBuilder() + .delete() + .from(protocolAgenda) + .where("id IN (:...ids)", { ids: removed.map((m) => m.id) }) + .andWhere({ protocolId }) .execute(); } }) diff --git a/src/command/club/protocol/protocolDecisionCommandHandler.ts b/src/command/club/protocol/protocolDecisionCommandHandler.ts index c466188..6e8b51c 100644 --- a/src/command/club/protocol/protocolDecisionCommandHandler.ts +++ b/src/command/club/protocol/protocolDecisionCommandHandler.ts @@ -33,12 +33,19 @@ export default abstract class ProtocolDecisionCommandHandler { } /** * @description sync protocolDecision + * @param {number} protocolId * @param {Array} syncProtocolDecisions * @returns {Promise} */ - static async sync(syncProtocolDecisions: Array): Promise { + static async sync( + protocolId: number, + syncProtocolDecisions: Array + ): Promise { + let currentDecision = await ProtocolDecisionService.getAll(protocolId); return await dataSource .transaction(async (transactionalEntityManager) => { + let removed = currentDecision.filter((ca) => !syncProtocolDecisions.some((spa) => spa.id == ca.id)); + for (const decision of syncProtocolDecisions) { await transactionalEntityManager .createQueryBuilder() @@ -47,8 +54,17 @@ export default abstract class ProtocolDecisionCommandHandler { .where({ id: decision.id }) .execute(); } + + if (removed.length != 0) { + await transactionalEntityManager + .createQueryBuilder() + .delete() + .from(protocolDecision) + .where("id IN (:...ids)", { ids: removed.map((m) => m.id) }) + .andWhere({ protocolId }) + .execute(); + } }) - .then(() => {}) .catch((err) => { throw new DatabaseActionException("SYNC", "protocolDecision", err); }); diff --git a/src/command/club/protocol/protocolVotingCommandHandler.ts b/src/command/club/protocol/protocolVotingCommandHandler.ts index 7a27cb6..a788ae5 100644 --- a/src/command/club/protocol/protocolVotingCommandHandler.ts +++ b/src/command/club/protocol/protocolVotingCommandHandler.ts @@ -33,12 +33,16 @@ export default abstract class ProtocolVotingCommandHandler { } /** * @description sync protocolVoting + * @param {number} protocolId * @param {Array} syncProtocolVotings * @returns {Promise} */ - static async sync(syncProtocolVotings: Array): Promise { + static async sync(protocolId: number, syncProtocolVotings: Array): Promise { + let currentVoting = await ProtocolVotingService.getAll(protocolId); return await dataSource .transaction(async (transactionalEntityManager) => { + let removed = currentVoting.filter((ca) => !syncProtocolVotings.some((spa) => spa.id == ca.id)); + for (const voting of syncProtocolVotings) { await transactionalEntityManager .createQueryBuilder() @@ -47,8 +51,17 @@ export default abstract class ProtocolVotingCommandHandler { .where({ id: voting.id }) .execute(); } + + if (removed.length != 0) { + await transactionalEntityManager + .createQueryBuilder() + .delete() + .from(protocolVoting) + .where("id IN (:...ids)", { ids: removed.map((m) => m.id) }) + .andWhere({ protocolId }) + .execute(); + } }) - .then(() => {}) .catch((err) => { throw new DatabaseActionException("SYNC", "protocolVoting", err); }); diff --git a/src/command/configuration/education/educationCommand.ts b/src/command/configuration/education/educationCommand.ts new file mode 100644 index 0000000..c0b10ad --- /dev/null +++ b/src/command/configuration/education/educationCommand.ts @@ -0,0 +1,14 @@ +export interface CreateEducationCommand { + education: string; + description?: string; +} + +export interface UpdateEducationCommand { + id: number; + education: string; + description?: string; +} + +export interface DeleteEducationCommand { + id: number; +} diff --git a/src/command/configuration/education/educationCommandHandler.ts b/src/command/configuration/education/educationCommandHandler.ts new file mode 100644 index 0000000..0c1eee9 --- /dev/null +++ b/src/command/configuration/education/educationCommandHandler.ts @@ -0,0 +1,68 @@ +import { dataSource } from "../../../data-source"; +import { education } from "../../../entity/configuration/education"; +import DatabaseActionException from "../../../exceptions/databaseActionException"; +import { CreateEducationCommand, DeleteEducationCommand, UpdateEducationCommand } from "./educationCommand"; + +export default abstract class EducationCommandHandler { + /** + * @description create education + * @param {CreateEducationCommand} createEducation + * @returns {Promise} + */ + static async create(createEducation: CreateEducationCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(education) + .values({ + education: createEducation.education, + description: createEducation.description, + }) + .execute() + .then((result) => { + return result.identifiers[0].id; + }) + .catch((err) => { + throw new DatabaseActionException("CREATE", "education", err); + }); + } + + /** + * @description update education + * @param {UpdateEducationCommand} updateEducation + * @returns {Promise} + */ + static async update(updateEducation: UpdateEducationCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(education) + .set({ + education: updateEducation.education, + description: updateEducation.description, + }) + .where("id = :id", { id: updateEducation.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("UPDATE", "education", err); + }); + } + + /** + * @description delete education + * @param {DeleteEducationCommand} deleteEducation + * @returns {Promise} + */ + static async delete(deleteEducation: DeleteEducationCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(education) + .where("id = :id", { id: deleteEducation.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "education", err); + }); + } +} diff --git a/src/command/configuration/newsletterConfig/newsletterConfigCommand.ts b/src/command/configuration/newsletterConfig/newsletterConfigCommand.ts index f07de54..5573062 100644 --- a/src/command/configuration/newsletterConfig/newsletterConfigCommand.ts +++ b/src/command/configuration/newsletterConfig/newsletterConfigCommand.ts @@ -1,8 +1,8 @@ -import { NewsletterConfigType } from "../../../enums/newsletterConfigType"; +import { NewsletterConfigEnum } from "../../../enums/newsletterConfigEnum"; export interface SetNewsletterConfigCommand { comTypeId: number; - config: NewsletterConfigType; + config: NewsletterConfigEnum; } export interface DeleteNewsletterConfigCommand { diff --git a/src/command/management/setting/settingCommand.ts b/src/command/management/setting/settingCommand.ts new file mode 100644 index 0000000..e0c9d84 --- /dev/null +++ b/src/command/management/setting/settingCommand.ts @@ -0,0 +1,10 @@ +export interface CreateOrUpdateSettingCommand { + topic: string; + key: string; + value: string; +} + +export interface DeleteSettingCommand { + topic: string; + key: string; +} diff --git a/src/command/management/setting/settingCommandHandler.ts b/src/command/management/setting/settingCommandHandler.ts new file mode 100644 index 0000000..9890f35 --- /dev/null +++ b/src/command/management/setting/settingCommandHandler.ts @@ -0,0 +1,50 @@ +import { dataSource } from "../../../data-source"; +import { setting } from "../../../entity/management/setting"; +import DatabaseActionException from "../../../exceptions/databaseActionException"; +import { CreateOrUpdateSettingCommand, DeleteSettingCommand } from "./settingCommand"; + +export default abstract class SettingCommandHandler { + /** + * @description create setting + * @param {CreateOrUpdateSettingCommand} createSetting + * @returns {Promise} + */ + static async create(createSetting: CreateOrUpdateSettingCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(setting) + .values({ + topic: createSetting.topic, + key: createSetting.key, + value: createSetting.value, + }) + .orUpdate(["value"], ["topic", "key"]) + .execute() + .then((result) => { + return createSetting.value; + }) + .catch((err) => { + throw new DatabaseActionException("CREATE OR UPDATE", "setting", err); + }); + } + + /** + * @description delete setting by topic and key + * @param {DeleteRefreshCommand} deleteSetting + * @returns {Promise} + */ + static async delete(deleteSetting: DeleteSettingCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(setting) + .where("setting.topic = :topic", { topic: deleteSetting.topic }) + .andWhere("setting.key = :key", { key: deleteSetting.key }) + .execute() + .then((res) => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "setting", err); + }); + } +} diff --git a/src/command/management/user/inviteCommandHandler.ts b/src/command/management/user/inviteCommandHandler.ts index 5c01044..2e341c2 100644 --- a/src/command/management/user/inviteCommandHandler.ts +++ b/src/command/management/user/inviteCommandHandler.ts @@ -26,7 +26,7 @@ export default abstract class InviteCommandHandler { lastname: createInvite.lastname, secret: createInvite.secret, }) - .orUpdate(["firstName", "lastName", "token", "secret"], ["mail"]) + .orUpdate(["firstname", "lastname", "token", "secret"], ["mail"]) .execute() .then((result) => { return token; diff --git a/src/command/management/user/userCommand.ts b/src/command/management/user/userCommand.ts index 90f9872..cb19989 100644 --- a/src/command/management/user/userCommand.ts +++ b/src/command/management/user/userCommand.ts @@ -1,3 +1,5 @@ +import { LoginRoutineEnum } from "../../../enums/loginRoutineEnum"; + export interface CreateUserCommand { mail: string; username: string; @@ -5,6 +7,7 @@ export interface CreateUserCommand { lastname: string; secret: string; isOwner: boolean; + routine: LoginRoutineEnum; } export interface UpdateUserCommand { @@ -18,6 +21,7 @@ export interface UpdateUserCommand { export interface UpdateUserSecretCommand { id: string; secret: string; + routine: LoginRoutineEnum; } export interface TransferUserOwnerCommand { diff --git a/src/command/management/user/userCommandHandler.ts b/src/command/management/user/userCommandHandler.ts index 590b2de..daa5535 100644 --- a/src/command/management/user/userCommandHandler.ts +++ b/src/command/management/user/userCommandHandler.ts @@ -31,6 +31,7 @@ export default abstract class UserCommandHandler { lastname: createUser.lastname, secret: createUser.secret, isOwner: createUser.isOwner, + routine: createUser.routine, }) .execute() .then((result) => { @@ -75,6 +76,7 @@ export default abstract class UserCommandHandler { .update(user) .set({ secret: updateUser.secret, + routine: updateUser.routine, }) .where("id = :id", { id: updateUser.id }) .execute() diff --git a/src/command/refreshCommandHandler.ts b/src/command/refreshCommandHandler.ts index df6a8ea..15f4f77 100644 --- a/src/command/refreshCommandHandler.ts +++ b/src/command/refreshCommandHandler.ts @@ -1,10 +1,8 @@ import { dataSource } from "../data-source"; import { refresh } from "../entity/refresh"; -import { PWA_REFRESH_EXPIRATION, REFRESH_EXPIRATION } from "../env.defaults"; import DatabaseActionException from "../exceptions/databaseActionException"; -import InternalException from "../exceptions/internalException"; +import SettingHelper from "../helpers/settingsHelper"; import { StringHelper } from "../helpers/stringHelper"; -import UserService from "../service/management/userService"; import { CreateRefreshCommand, DeleteRefreshCommand } from "./refreshCommand"; import ms from "ms"; @@ -25,8 +23,8 @@ export default abstract class RefreshCommandHandler { token: refreshToken, userId: createRefresh.userId, expiry: createRefresh.isFromPwa - ? new Date(Date.now() + ms(PWA_REFRESH_EXPIRATION)) - : new Date(Date.now() + ms(REFRESH_EXPIRATION)), + ? new Date(Date.now() + ms(SettingHelper.getSetting("session.pwa_refresh_expiration"))) + : new Date(Date.now() + ms(SettingHelper.getSetting("session.refresh_expiration"))), }) .execute() .then((result) => { diff --git a/src/controller/admin/club/memberController.ts b/src/controller/admin/club/memberController.ts index 9ef0bf4..6301cec 100644 --- a/src/controller/admin/club/memberController.ts +++ b/src/controller/admin/club/memberController.ts @@ -49,6 +49,14 @@ import { import CommunicationCommandHandler from "../../../command/club/member/communicationCommandHandler"; import { PdfExport } from "../../../helpers/pdfExport"; import { PermissionModule } from "../../../type/permissionTypes"; +import MemberEducationFactory from "../../../factory/admin/club/member/memberEducation"; +import MemberEducationService from "../../../service/club/member/memberEducationService"; +import { + CreateMemberEducationCommand, + DeleteMemberEducationCommand, + UpdateMemberEducationCommand, +} from "../../../command/club/member/memberEducationCommand"; +import MemberEducationCommandHandler from "../../../command/club/member/memberEducationCommandHandler"; /** * @description get all members @@ -92,6 +100,18 @@ export async function getMembersByIds(req: Request, res: Response): Promise }); } +/** + * @description get member latest inserted InternalId + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getMemberLastInternalId(req: Request, res: Response): Promise { + let latest = await MemberService.getLatestInternalId(); + + res.send(latest); +} + /** * @description get member by id * @param req {Request} Express req object @@ -132,6 +152,7 @@ export async function getMemberPrintoutById(req: Request, res: Response): Promis let qualifications = await MemberQualificationService.getAll(memberId); let positions = await MemberExecutivePositionService.getAll(memberId); let communications = await CommunicationService.getAll(memberId); + let educations = await MemberEducationService.getAll(memberId); let pdf = await PdfExport.renderFile({ title: "Mitglieder-Ausdruck", @@ -145,6 +166,7 @@ export async function getMemberPrintoutById(req: Request, res: Response): Promis qualifications, positions, communications, + educations, }, }); @@ -183,6 +205,19 @@ export async function getMembershipStatisticsById(req: Request, res: Response): res.json(MembershipFactory.mapToBaseStatistics(member)); } +/** + * @description get member total statistics by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getMembershipTotalStatisticsById(req: Request, res: Response): Promise { + const memberId = req.params.memberId; + let member = await MembershipService.getTotalStatisticsById(memberId); + + res.json(MembershipFactory.mapToSingleTotalStatistic(member)); +} + /** * @description get membership by member and record * @param req {Request} Express req object @@ -251,6 +286,33 @@ export async function getQualificationByMemberAndRecord(req: Request, res: Respo res.json(MemberQualificationFactory.mapToSingle(qualification)); } +/** + * @description get educations by member + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getEducationsByMember(req: Request, res: Response): Promise { + const memberId = req.params.memberId; + let educations = await MemberEducationService.getAll(memberId); + + res.json(MemberEducationFactory.mapToBase(educations)); +} + +/** + * @description get education by member and record + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getEducationByMemberAndRecord(req: Request, res: Response): Promise { + const memberId = req.params.memberId; + const recordId = parseInt(req.params.id); + let education = await MemberEducationService.getById(memberId, recordId); + + res.json(MemberEducationFactory.mapToSingle(education)); +} + /** * @description get executive positions by member * @param req {Request} Express req object @@ -318,6 +380,7 @@ export async function createMember(req: Request, res: Response): Promise { const nameaffix = req.body.nameaffix; const birthdate = req.body.birthdate; const internalId = req.body.internalId || null; + const note = req.body.note || null; let createMember: CreateMemberCommand = { salutationId, @@ -326,6 +389,7 @@ export async function createMember(req: Request, res: Response): Promise { nameaffix, birthdate, internalId, + note, }; let memberId = await MemberCommandHandler.create(createMember); @@ -401,6 +465,33 @@ export async function addQualificationToMember(req: Request, res: Response): Pro res.sendStatus(204); } +/** + * @description add education to member + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function addEducationToMember(req: Request, res: Response): Promise { + const memberId = req.params.memberId; + const note = req.body.note; + const place = req.body.place; + const start = req.body.start; + const end = req.body.end || null; + const educationId = req.body.educationId; + + let createMemberEducation: CreateMemberEducationCommand = { + note, + start, + end, + place, + memberId, + educationId, + }; + await MemberEducationCommandHandler.create(createMemberEducation); + + res.sendStatus(204); +} + /** * @description add executive positions to member * @param req {Request} Express req object @@ -479,6 +570,7 @@ export async function updateMemberById(req: Request, res: Response): Promise} + */ +export async function updateEducationOfMember(req: Request, res: Response): Promise { + const memberId = req.params.memberId; + const recordId = parseInt(req.params.recordId); + const start = req.body.start; + const end = req.body.end || null; + const note = req.body.note; + const place = req.body.place; + const educationId = req.body.educationId; + + let updateMemberEducation: UpdateMemberEducationCommand = { + id: recordId, + start, + end, + note, + place, + memberId, + educationId, + }; + await MemberEducationCommandHandler.update(updateMemberEducation); + + res.sendStatus(204); +} + /** * @description update executive position of member * @param req {Request} Express req object @@ -717,6 +839,25 @@ export async function deleteQualificationOfMember(req: Request, res: Response): res.sendStatus(204); } +/** + * @description delete education from member + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function deleteEducationsOfMember(req: Request, res: Response): Promise { + const memberId = req.params.memberId; + const recordId = parseInt(req.params.recordId); + + let deleteMemberEducation: DeleteMemberEducationCommand = { + id: recordId, + memberId, + }; + await MemberEducationCommandHandler.delete(deleteMemberEducation); + + res.sendStatus(204); +} + /** * @description delete executive position from member * @param req {Request} Express req object diff --git a/src/controller/admin/club/protocolController.ts b/src/controller/admin/club/protocolController.ts index 98396c8..4edfd88 100644 --- a/src/controller/admin/club/protocolController.ts +++ b/src/controller/admin/club/protocolController.ts @@ -249,12 +249,7 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P title: protocol.title, summary: protocol.summary, iteration: iteration + 1, - date: new Date(protocol.date).toLocaleDateString("de-DE", { - weekday: "long", - day: "2-digit", - month: "2-digit", - year: "numeric", - }), + date: protocol.date, start: protocol.starttime, end: protocol.endtime, agenda: agenda.sort((a, b) => a.sort - b.sort), @@ -324,7 +319,7 @@ export async function synchronizeProtocolAgendaById(req: Request, res: Response) protocolId, }) ); - await ProtocolAgendaCommandHandler.sync(syncAgenda); + await ProtocolAgendaCommandHandler.sync(protocolId, syncAgenda); res.sendStatus(204); } @@ -348,7 +343,7 @@ export async function synchronizeProtocolDecisonsById(req: Request, res: Respons protocolId, }) ); - await ProtocolDecisionCommandHandler.sync(syncDecision); + await ProtocolDecisionCommandHandler.sync(protocolId, syncDecision); res.sendStatus(204); } @@ -375,7 +370,7 @@ export async function synchronizeProtocolVotingsById(req: Request, res: Response protocolId, }) ); - await ProtocolVotingCommandHandler.sync(syncVoting); + await ProtocolVotingCommandHandler.sync(protocolId, syncVoting); res.sendStatus(204); } diff --git a/src/controller/admin/configuration/educationController.ts b/src/controller/admin/configuration/educationController.ts new file mode 100644 index 0000000..b8f2e70 --- /dev/null +++ b/src/controller/admin/configuration/educationController.ts @@ -0,0 +1,91 @@ +import { Request, Response } from "express"; +import EducationService from "../../../service/configuration/education"; +import EducationFactory from "../../../factory/admin/configuration/education"; +import { + CreateEducationCommand, + DeleteEducationCommand, + UpdateEducationCommand, +} from "../../../command/configuration/education/educationCommand"; +import EducationCommandHandler from "../../../command/configuration/education/educationCommandHandler"; + +/** + * @description get all educations + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllEducations(req: Request, res: Response): Promise { + let educations = await EducationService.getAll(); + + res.json(EducationFactory.mapToBase(educations)); +} + +/** + * @description get education by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getEducationById(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + let education = await EducationService.getById(id); + + res.json(EducationFactory.mapToSingle(education)); +} + +/** + * @description create new education + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createEducation(req: Request, res: Response): Promise { + const education = req.body.education; + const description = req.body.description; + + let createEducation: CreateEducationCommand = { + education: education, + description: description, + }; + await EducationCommandHandler.create(createEducation); + + res.sendStatus(204); +} + +/** + * @description update education + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateEducation(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + const education = req.body.education; + const description = req.body.description; + + let updateEducation: UpdateEducationCommand = { + id: id, + education: education, + description: description, + }; + await EducationCommandHandler.update(updateEducation); + + res.sendStatus(204); +} + +/** + * @description delete education + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function deleteEducation(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + + let deleteEducation: DeleteEducationCommand = { + id: id, + }; + await EducationCommandHandler.delete(deleteEducation); + + res.sendStatus(204); +} diff --git a/src/controller/admin/management/settingController.ts b/src/controller/admin/management/settingController.ts new file mode 100644 index 0000000..643ba44 --- /dev/null +++ b/src/controller/admin/management/settingController.ts @@ -0,0 +1,116 @@ +import { Request, Response } from "express"; +import SettingHelper from "../../../helpers/settingsHelper"; +import { SettingString, SettingValueMapping } from "../../../type/settingTypes"; +import MailHelper from "../../../helpers/mailHelper"; +import InternalException from "../../../exceptions/internalException"; + +/** + * @description get All settings + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getSettings(req: Request, res: Response): Promise { + res.json({ ...SettingHelper.getAllSettings(), ["mail.password"]: undefined }); +} + +/** + * @description get setting + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getSetting(req: Request, res: Response): Promise { + let setting = req.params.setting as SettingString; + + let value = SettingHelper.getSetting(setting); + + if (setting == "mail.password") { + value = undefined; + } + + res.send(value); +} + +/** + * @description set setting + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function setSetting(req: Request, res: Response): Promise { + let setting = req.body.setting as SettingString; + let value = req.body.value as string; + + await SettingHelper.checkMail([{ key: setting, value }]).catch((err) => { + if (err == "mail") { + throw new InternalException("Mail is not valid"); + } else { + throw new InternalException("Config is not valid"); + } + }); + + await SettingHelper.setSetting(setting, value); + + res.sendStatus(204); +} + +/** + * @description set settings + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function setSettings(req: Request, res: Response): Promise { + let setting = req.body as Array<{ key: K; value: SettingValueMapping[K] }>; + + await SettingHelper.checkMail(setting).catch((err) => { + if (err == "mail") { + throw new InternalException("Mail is not valid"); + } else { + throw new InternalException("Config is not valid"); + } + }); + + for (let entry of setting) { + await SettingHelper.setSetting(entry.key, entry.value); + } + + res.sendStatus(204); +} + +/** + * @description set setting + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function setImages(req: Request, res: Response): Promise { + if (req.files && !Array.isArray(req.files) && req.files.icon) { + await SettingHelper.setSetting("club.icon", "configured"); + } else if (req.body["club.icon"] != "keep") { + await SettingHelper.resetSetting("club.icon"); + } + + if (req.files && !Array.isArray(req.files) && req.files.logo) { + await SettingHelper.setSetting("club.logo", "configured"); + } else if (req.body["club.logo"] != "keep") { + await SettingHelper.resetSetting("club.logo"); + } + + res.sendStatus(204); +} + +/** + * @description reset setting + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function resetSetting(req: Request, res: Response): Promise { + let setting = req.params.setting as SettingString; + + await SettingHelper.resetSetting(setting); + + res.sendStatus(204); +} diff --git a/src/controller/admin/management/userController.ts b/src/controller/admin/management/userController.ts index ba2a134..7da2460 100644 --- a/src/controller/admin/management/userController.ts +++ b/src/controller/admin/management/userController.ts @@ -11,10 +11,10 @@ import { } from "../../../command/management/user/userCommand"; import UserCommandHandler from "../../../command/management/user/userCommandHandler"; import MailHelper from "../../../helpers/mailHelper"; -import { CLUB_NAME } from "../../../env.defaults"; import { UpdateUserPermissionsCommand } from "../../../command/management/user/userPermissionCommand"; import UserPermissionCommandHandler from "../../../command/management/user/userPermissionCommandHandler"; import BadRequestException from "../../../exceptions/badRequestException"; +import SettingHelper from "../../../helpers/settingsHelper"; /** * @description get All users @@ -157,7 +157,7 @@ export async function deleteUser(req: Request, res: Response): Promise { // sendmail await MailHelper.sendMail( mail, - `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, + `Email Bestätigung für Mitglieder Admin-Portal von ${SettingHelper.getSetting("club.name")}`, `Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.` ); } catch (error) {} diff --git a/src/controller/admin/management/webapiController.ts b/src/controller/admin/management/webapiController.ts index 80df60a..75a3011 100644 --- a/src/controller/admin/management/webapiController.ts +++ b/src/controller/admin/management/webapiController.ts @@ -12,8 +12,8 @@ import WebapiCommandHandler from "../../../command/management/webapi/webapiComma import { UpdateWebapiPermissionsCommand } from "../../../command/management/webapi/webapiPermissionCommand"; import WebapiPermissionCommandHandler from "../../../command/management/webapi/webapiPermissionCommandHandler"; import { JWTHelper } from "../../../helpers/jwtHelper"; -import { CLUB_NAME } from "../../../env.defaults"; import { StringHelper } from "../../../helpers/stringHelper"; +import SettingHelper from "../../../helpers/settingsHelper"; /** * @description get All apis @@ -78,7 +78,7 @@ export async function createWebapi(req: Request, res: Response): Promise { let token = await JWTHelper.create( { - iss: CLUB_NAME, + iss: SettingHelper.getSetting("club.name"), sub: "api_token_retrieve", aud: StringHelper.random(32), }, diff --git a/src/controller/authController.ts b/src/controller/authController.ts index 9ecfa64..ce674ab 100644 --- a/src/controller/authController.ts +++ b/src/controller/authController.ts @@ -8,6 +8,25 @@ import UserService from "../service/management/userService"; import speakeasy from "speakeasy"; import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; import RefreshService from "../service/refreshService"; +import { LoginRoutineEnum } from "../enums/loginRoutineEnum"; + +/** + * @description Check authentication status by token + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function kickof(req: Request, res: Response): Promise { + let username = req.body.username; + + let { routine } = await UserService.getByUsername(username).catch(() => { + throw new UnauthorizedRequestException("Username not found"); + }); + + res.json({ + routine, + }); +} /** * @description Check authentication status by token @@ -17,19 +36,25 @@ import RefreshService from "../service/refreshService"; */ export async function login(req: Request, res: Response): Promise { let username = req.body.username; - let totp = req.body.totp; + let passedSecret = req.body.secret; - let { id, secret } = await UserService.getByUsername(username); + let { id } = await UserService.getByUsername(username); + let { secret, routine } = await UserService.getUserSecretAndRoutine(id); - let valid = speakeasy.totp.verify({ - secret: secret, - encoding: "base32", - token: totp, - window: 2, - }); + let valid = false; + if (routine == LoginRoutineEnum.totp) { + valid = speakeasy.totp.verify({ + secret: secret, + encoding: "base32", + token: passedSecret, + window: 2, + }); + } else { + valid = passedSecret == secret; + } if (!valid) { - throw new UnauthorizedRequestException("Token not valid or expired"); + throw new UnauthorizedRequestException("Credentials not valid or expired"); } let accessToken = await JWTHelper.buildToken(id); diff --git a/src/controller/inviteController.ts b/src/controller/inviteController.ts index 14e346c..9492e10 100644 --- a/src/controller/inviteController.ts +++ b/src/controller/inviteController.ts @@ -1,6 +1,5 @@ import { Request, Response } from "express"; import { JWTHelper } from "../helpers/jwtHelper"; -import { JWTToken } from "../type/jwtTypes"; import InternalException from "../exceptions/internalException"; import RefreshCommandHandler from "../command/refreshCommandHandler"; import { CreateRefreshCommand } from "../command/refreshCommand"; @@ -15,10 +14,9 @@ import MailHelper from "../helpers/mailHelper"; import InviteService from "../service/management/inviteService"; import UserService from "../service/management/userService"; import CustomRequestException from "../exceptions/customRequestException"; -import { CLUB_NAME } from "../env.defaults"; -import { CreateUserPermissionCommand } from "../command/management/user/userPermissionCommand"; -import UserPermissionCommandHandler from "../command/management/user/userPermissionCommandHandler"; import InviteFactory from "../factory/admin/management/invite"; +import SettingHelper from "../helpers/settingsHelper"; +import { LoginRoutineEnum } from "../enums/loginRoutineEnum"; /** * @description get all invites @@ -38,7 +36,7 @@ export async function getInvites(req: Request, res: Response): Promise { * @param res {Response} Express res object * @returns {Promise<*>} */ -export async function inviteUser(req: Request, res: Response, isInvite: boolean = true): Promise { +export async function inviteUser(req: Request, res: Response, isSetup: boolean = false): Promise { let origin = req.headers.origin; let username = req.body.username; let mail = req.body.mail; @@ -59,7 +57,7 @@ export async function inviteUser(req: Request, res: Response, isInvite: boolean throw new CustomRequestException(409, "Username and Mail are already in use"); } - var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${CLUB_NAME}` }); + var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${SettingHelper.getSetting("club.name")}` }); let createInvite: CreateInviteCommand = { username: username, @@ -73,8 +71,8 @@ export async function inviteUser(req: Request, res: Response, isInvite: boolean // sendmail await MailHelper.sendMail( mail, - `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, - `Öffne folgenden Link: ${origin}/${isInvite ? "invite" : "setup"}/verify?mail=${mail}&token=${token}` + `Email Bestätigung für Mitglieder Admin-Portal von ${SettingHelper.getSetting("club.name")}`, + `Öffne folgenden Link: ${origin}/${isSetup ? "setup" : "invite"}/verify?mail=${mail}&token=${token}` ); res.sendStatus(204); @@ -92,7 +90,7 @@ export async function verifyInvite(req: Request, res: Response): Promise { let { secret, username } = await InviteService.getByMailAndToken(mail, token); - const url = `otpauth://totp/FF Admin ${CLUB_NAME}?secret=${secret}`; + const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`; QRCode.toDataURL(url) .then((result) => { @@ -115,20 +113,26 @@ export async function verifyInvite(req: Request, res: Response): Promise { */ export async function finishInvite(req: Request, res: Response, grantAdmin: boolean = false): Promise { let mail = req.body.mail; + let routine = req.body.routine; let token = req.body.token; - let totp = req.body.totp; + let passedSecret = req.body.secret; let { secret, username, firstname, lastname } = await InviteService.getByMailAndToken(mail, token); - let valid = speakeasy.totp.verify({ - secret: secret, - encoding: "base32", - token: totp, - window: 2, - }); + let valid = false; + if (routine == LoginRoutineEnum.totp) { + valid = speakeasy.totp.verify({ + secret: secret, + encoding: "base32", + token: passedSecret, + window: 2, + }); + } else { + valid = passedSecret != ""; + } if (!valid) { - throw new UnauthorizedRequestException("Token not valid or expired"); + throw new UnauthorizedRequestException("Credentials not valid or expired"); } let createUser: CreateUserCommand = { @@ -136,8 +140,9 @@ export async function finishInvite(req: Request, res: Response, grantAdmin: bool firstname: firstname, lastname: lastname, mail: mail, - secret: secret, + secret: routine == LoginRoutineEnum.totp ? secret : passedSecret, isOwner: grantAdmin, + routine, }; let id = await UserCommandHandler.create(createUser); diff --git a/src/controller/publicController.ts b/src/controller/publicController.ts index f44100d..e48c008 100644 --- a/src/controller/publicController.ts +++ b/src/controller/publicController.ts @@ -2,11 +2,13 @@ import { Request, Response } from "express"; import CalendarService from "../service/club/calendarService"; import CalendarTypeService from "../service/configuration/calendarTypeService"; import { calendar } from "../entity/club/calendar"; -import { createEvents } from "ics"; -import moment from "moment"; import InternalException from "../exceptions/internalException"; import CalendarFactory from "../factory/admin/club/calendar"; import { CalendarHelper } from "../helpers/calendarHelper"; +import SettingHelper from "../helpers/settingsHelper"; +import sharp from "sharp"; +import ico from "sharp-ico"; +import { FileSystemHelper } from "../helpers/fileSystemHelper"; /** * @description get all calendar items by types or nscdr @@ -51,3 +53,155 @@ export async function getCalendarItemsByTypes(req: Request, res: Response): Prom res.type("ics").send(value); } } + +/** + * @description get configuration of UI + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getApplicationConfig(req: Request, res: Response): Promise { + let config = { + "club.name": SettingHelper.getSetting("club.name"), + "club.imprint": SettingHelper.getSetting("club.imprint"), + "club.privacy": SettingHelper.getSetting("club.privacy"), + "club.website": SettingHelper.getSetting("club.website"), + "app.custom_login_message": SettingHelper.getSetting("app.custom_login_message"), + "app.show_link_to_calendar": SettingHelper.getSetting("app.show_link_to_calendar"), + }; + + res.json(config); +} + +/** + * @description get application Manifest + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getApplicationManifest(req: Request, res: Response): Promise { + const backendUrl = `${req.protocol}://${req.get("host")}`; + const frontenUrl = `${req.get("referer")}`; + + const manifest = { + id: "ff_admin_webapp", + lang: "de", + name: SettingHelper.getSetting("club.name"), + short_name: SettingHelper.getSetting("club.name"), + theme_color: "#990b00", + display: "standalone", + orientation: "portrait-primary", + start_url: frontenUrl, + icons: [ + { + src: `${backendUrl}/api/public/favicon.ico`, + sizes: "48x48", + type: "image/ico", + }, + { + src: `${backendUrl}/api/public/icon.png?width=512&height=512`, + sizes: "512x512", + type: "image/png", + }, + ], + }; + + res.set({ + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/manifest+json", + }); + + res.json(manifest); +} + +/** + * @description get application Logo + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getApplicationLogo(req: Request, res: Response): Promise { + let setLogo = SettingHelper.getSetting("club.logo"); + + res.set({ + "Access-Control-Allow-Origin": "*", + "Cross-Origin-Resource-Policy": "cross-origin", + "Cross-Origin-Embedder-Policy": "credentialless", + "Timing-Allow-Origin": "*", + }); + + if (setLogo != "" && FileSystemHelper.getFilesInDirectory("/app", ".png").includes("admin-icon.png")) { + res.sendFile(FileSystemHelper.formatPath("/app/admin-logo.png")); + } else { + res.sendFile(FileSystemHelper.readAssetFile("admin-logo.png", true)); + } +} + +/** + * @description get application Favicon + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getApplicationFavicon(req: Request, res: Response): Promise { + let icon = FileSystemHelper.readAssetFile("icon.png", true); + let setLogo = SettingHelper.getSetting("club.icon"); + + if (setLogo != "" && FileSystemHelper.getFilesInDirectory("/app", ".png").includes("admin-icon.png")) { + icon = FileSystemHelper.formatPath("/app/admin-icon.png"); + } + + let image = await sharp(icon) + .resize(48, 48, { + fit: "inside", + }) + .png() + .toBuffer(); + + let buffer = ico.encode([image]); + + res.set({ + "Access-Control-Allow-Origin": "*", + "Cross-Origin-Resource-Policy": "cross-origin", + "Cross-Origin-Embedder-Policy": "credentialless", + "Timing-Allow-Origin": "*", + "Content-Type": "image/x-icon", + }); + + res.send(buffer); +} + +/** + * @description get application Icon + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getApplicationIcon(req: Request, res: Response): Promise { + const width = parseInt((req.query.width as string) ?? "512"); + const height = parseInt((req.query.height as string) ?? "512"); + + let icon = FileSystemHelper.readAssetFile("icon.png", true); + let setLogo = SettingHelper.getSetting("club.icon"); + + if (setLogo != "" && FileSystemHelper.getFilesInDirectory("/app", ".png").includes("admin-icon.png")) { + icon = FileSystemHelper.formatPath("/app/admin-icon.png"); + } + + let image = await sharp(icon) + .resize(width, height, { + fit: "inside", + }) + .png() + .toBuffer(); + + res.set({ + "Access-Control-Allow-Origin": "*", + "Cross-Origin-Resource-Policy": "cross-origin", + "Cross-Origin-Embedder-Policy": "credentialless", + "Timing-Allow-Origin": "*", + "Content-Type": "image/png", + }); + + res.send(image); +} diff --git a/src/controller/resetController.ts b/src/controller/resetController.ts index fc0dc3e..7ffafe5 100644 --- a/src/controller/resetController.ts +++ b/src/controller/resetController.ts @@ -1,6 +1,5 @@ import { Request, Response } from "express"; import { JWTHelper } from "../helpers/jwtHelper"; -import { JWTToken } from "../type/jwtTypes"; import InternalException from "../exceptions/internalException"; import RefreshCommandHandler from "../command/refreshCommandHandler"; import { CreateRefreshCommand } from "../command/refreshCommand"; @@ -12,12 +11,10 @@ import ResetCommandHandler from "../command/resetCommandHandler"; import MailHelper from "../helpers/mailHelper"; import ResetService from "../service/resetService"; import UserService from "../service/management/userService"; -import { CLUB_NAME } from "../env.defaults"; -import PermissionHelper from "../helpers/permissionHelper"; -import RolePermissionService from "../service/management/rolePermissionService"; -import UserPermissionService from "../service/management/userPermissionService"; import { UpdateUserSecretCommand } from "../command/management/user/userCommand"; import UserCommandHandler from "../command/management/user/userCommandHandler"; +import SettingHelper from "../helpers/settingsHelper"; +import { LoginRoutineEnum } from "../enums/loginRoutineEnum"; /** * @description request totp reset @@ -31,7 +28,7 @@ export async function startReset(req: Request, res: Response): Promise { let { mail } = await UserService.getByUsername(username); - var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${CLUB_NAME}` }); + var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${SettingHelper.getSetting("club.name")}` }); let createReset: CreateResetCommand = { username: username, @@ -43,7 +40,7 @@ export async function startReset(req: Request, res: Response): Promise { // sendmail await MailHelper.sendMail( mail, - `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, + `Email Bestätigung für Mitglieder Admin-Portal von ${SettingHelper.getSetting("club.name")}`, `Öffne folgenden Link: ${origin}/reset/reset?mail=${mail}&token=${token}` ); @@ -62,7 +59,7 @@ export async function verifyReset(req: Request, res: Response): Promise { let { secret } = await ResetService.getByMailAndToken(mail, token); - const url = `otpauth://totp/FF Admin ${CLUB_NAME}?secret=${secret}`; + const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`; QRCode.toDataURL(url) .then((result) => { @@ -84,27 +81,34 @@ export async function verifyReset(req: Request, res: Response): Promise { */ export async function finishReset(req: Request, res: Response): Promise { let mail = req.body.mail; + let routine = req.body.routine; let token = req.body.token; - let totp = req.body.totp; + let passedSecret = req.body.secret; let { secret, username } = await ResetService.getByMailAndToken(mail, token); - let valid = speakeasy.totp.verify({ - secret: secret, - encoding: "base32", - token: totp, - window: 2, - }); + let valid = false; + if (routine == LoginRoutineEnum.totp) { + valid = speakeasy.totp.verify({ + secret: secret, + encoding: "base32", + token: passedSecret, + window: 2, + }); + } else { + valid = passedSecret != ""; + } if (!valid) { - throw new UnauthorizedRequestException("Token not valid or expired"); + throw new UnauthorizedRequestException("Credentials not valid or expired"); } let { id } = await UserService.getByUsername(username); let updateUserSecret: UpdateUserSecretCommand = { id, - secret, + secret: routine == LoginRoutineEnum.totp ? secret : passedSecret, + routine, }; await UserCommandHandler.updateSecret(updateUserSecret); diff --git a/src/controller/setupController.ts b/src/controller/setupController.ts index f71194f..f64ca65 100644 --- a/src/controller/setupController.ts +++ b/src/controller/setupController.ts @@ -1,4 +1,7 @@ import { Request, Response } from "express"; +import SettingHelper from "../helpers/settingsHelper"; +import MailHelper from "../helpers/mailHelper"; +import InternalException from "../exceptions/internalException"; /** * @description Service is currently not configured @@ -9,3 +12,131 @@ import { Request, Response } from "express"; export async function isSetup(req: Request, res: Response): Promise { res.sendStatus(204); } + +/** + * @description set club identity + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function setClubIdentity(req: Request, res: Response): Promise { + const name = req.body.name; + const imprint = req.body.imprint; + const privacy = req.body.privacy; + const website = req.body.website; + + if (name) { + await SettingHelper.setSetting("club.name", name); + } else { + await SettingHelper.resetSetting("club.name"); + } + + if (imprint) { + await SettingHelper.setSetting("club.imprint", imprint); + } else { + await SettingHelper.resetSetting("club.imprint"); + } + + if (privacy) { + await SettingHelper.setSetting("club.privacy", privacy); + } else { + await SettingHelper.resetSetting("club.privacy"); + } + + if (website) { + await SettingHelper.setSetting("club.website", website); + } else { + await SettingHelper.resetSetting("club.website"); + } + + res.sendStatus(204); +} + +/** + * @description set applucation icon and logo + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function uploadClubImages(req: Request, res: Response): Promise { + if (req.files && !Array.isArray(req.files) && req.files.icon) { + await SettingHelper.setSetting("club.icon", "configured"); + } else { + await SettingHelper.resetSetting("club.icon"); + } + + if (req.files && !Array.isArray(req.files) && req.files.logo) { + await SettingHelper.setSetting("club.logo", "configured"); + } else { + await SettingHelper.resetSetting("club.logo"); + } + + res.sendStatus(204); +} + +/** + * @description set app identity + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function setAppIdentity(req: Request, res: Response): Promise { + const custom_login_message = req.body.custom_login_message; + const show_link_to_calendar = req.body.show_link_to_calendar; + + if (custom_login_message) { + await SettingHelper.setSetting("app.custom_login_message", custom_login_message); + } else { + await SettingHelper.resetSetting("app.custom_login_message"); + } + + if (show_link_to_calendar == false || show_link_to_calendar == true) { + await SettingHelper.setSetting("app.show_link_to_calendar", show_link_to_calendar); + } else { + await SettingHelper.resetSetting("app.show_link_to_calendar"); + } + + res.sendStatus(204); +} + +/** + * @description set app identity + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function setMailConfig(req: Request, res: Response): Promise { + const mail = req.body.mail; + const username = req.body.username; + const password = req.body.password; + const host = req.body.host; + const port = req.body.port; + const secure = req.body.secure; + + let checkMail = await MailHelper.checkMail(mail); + + if (!checkMail) { + throw new InternalException("Mail is not valid"); + } + + let checkConfig = await MailHelper.verifyTransport({ + user: username, + password, + host, + port, + secure, + }); + + if (!checkConfig) { + throw new InternalException("Config is not valid"); + } + + await SettingHelper.setSetting("mail.email", mail); + await SettingHelper.setSetting("mail.username", username); + await SettingHelper.setSetting("mail.password", password); + await SettingHelper.setSetting("mail.host", host); + await SettingHelper.setSetting("mail.port", port); + await SettingHelper.setSetting("mail.secure", secure); + + res.sendStatus(204); +} diff --git a/src/controller/userController.ts b/src/controller/userController.ts index 1e764f1..1fe92c5 100644 --- a/src/controller/userController.ts +++ b/src/controller/userController.ts @@ -2,12 +2,17 @@ import { Request, Response } from "express"; import speakeasy from "speakeasy"; import QRCode from "qrcode"; import InternalException from "../exceptions/internalException"; -import { CLUB_NAME } from "../env.defaults"; import UserService from "../service/management/userService"; import UserFactory from "../factory/admin/management/user"; -import { TransferUserOwnerCommand, UpdateUserCommand } from "../command/management/user/userCommand"; +import { + TransferUserOwnerCommand, + UpdateUserCommand, + UpdateUserSecretCommand, +} from "../command/management/user/userCommand"; import UserCommandHandler from "../command/management/user/userCommandHandler"; import ForbiddenRequestException from "../exceptions/forbiddenRequestException"; +import SettingHelper from "../helpers/settingsHelper"; +import { LoginRoutineEnum } from "../enums/loginRoutineEnum"; /** * @description get my by id @@ -22,6 +27,21 @@ export async function getMeById(req: Request, res: Response): Promise { res.json(UserFactory.mapToSingle(user)); } +/** + * @description get my routine by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getMyRoutine(req: Request, res: Response): Promise { + const id = req.userId; + let user = await UserService.getById(id); + + res.json({ + routine: user.routine, + }); +} + /** * @description get my totp * @param req {Request} Express req object @@ -31,9 +51,9 @@ export async function getMeById(req: Request, res: Response): Promise { export async function getMyTotp(req: Request, res: Response): Promise { const userId = req.userId; - let { secret } = await UserService.getById(userId); + let { secret, routine } = await UserService.getUserSecretAndRoutine(userId); - const url = `otpauth://totp/FF Admin ${CLUB_NAME}?secret=${secret}`; + const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`; QRCode.toDataURL(url) .then((result) => { @@ -57,7 +77,12 @@ export async function verifyMyTotp(req: Request, res: Response): Promise { const userId = req.userId; let totp = req.body.totp; - let { secret } = await UserService.getById(userId); + let { secret, routine } = await UserService.getUserSecretAndRoutine(userId); + + if (routine != LoginRoutineEnum.totp) { + throw new ForbiddenRequestException("only allowed for totp login"); + } + let valid = speakeasy.totp.verify({ secret: secret, encoding: "base32", @@ -71,6 +96,106 @@ export async function verifyMyTotp(req: Request, res: Response): Promise { res.sendStatus(204); } +/** + * @description change my password + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function changeMyPassword(req: Request, res: Response): Promise { + const userId = req.userId; + let current = req.body.current; + let newpassword = req.body.newpassword; + + let { secret, routine } = await UserService.getUserSecretAndRoutine(userId); + + if (routine == LoginRoutineEnum.password && current != secret) { + throw new ForbiddenRequestException("passwords do not match"); + } + + let updateUser: UpdateUserSecretCommand = { + id: userId, + secret: newpassword, + routine: LoginRoutineEnum.password, + }; + await UserCommandHandler.updateSecret(updateUser); + + res.sendStatus(204); +} + +/** + * @description get change to totp + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getChangeToTOTP(req: Request, res: Response): Promise { + var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${SettingHelper.getSetting("club.name")}` }); + + QRCode.toDataURL(secret.otpauth_url) + .then((result) => { + res.json({ + dataUrl: result, + otp: secret.base32, + }); + }) + .catch((err) => { + throw new InternalException("QRCode not created", err); + }); +} + +/** + * @description change to totp + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function changeToTOTP(req: Request, res: Response): Promise { + const userId = req.userId; + let otp = req.body.otp; + let totp = req.body.totp; + + let valid = speakeasy.totp.verify({ + secret: otp, + encoding: "base32", + token: totp, + window: 2, + }); + + if (!valid) { + throw new InternalException("Token not valid or expired"); + } + + let updateUser: UpdateUserSecretCommand = { + id: userId, + secret: otp, + routine: LoginRoutineEnum.totp, + }; + await UserCommandHandler.updateSecret(updateUser); + + res.sendStatus(204); +} + +/** + * @description change to password + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function changeToPW(req: Request, res: Response): Promise { + const userId = req.userId; + let newpassword = req.body.newpassword; + + let updateUser: UpdateUserSecretCommand = { + id: userId, + secret: newpassword, + routine: LoginRoutineEnum.password, + }; + await UserCommandHandler.updateSecret(updateUser); + + res.sendStatus(204); +} + /** * @description transferOwnership * @param req {Request} Express req object diff --git a/src/controller/webapiController.ts b/src/controller/webapiController.ts index e8e4206..c1153fe 100644 --- a/src/controller/webapiController.ts +++ b/src/controller/webapiController.ts @@ -1,13 +1,5 @@ import { Request, Response } from "express"; import { JWTHelper } from "../helpers/jwtHelper"; -import { JWTToken } from "../type/jwtTypes"; -import InternalException from "../exceptions/internalException"; -import RefreshCommandHandler from "../command/refreshCommandHandler"; -import { CreateRefreshCommand, DeleteRefreshCommand } from "../command/refreshCommand"; -import UserService from "../service/management/userService"; -import speakeasy from "speakeasy"; -import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; -import RefreshService from "../service/refreshService"; import WebapiService from "../service/management/webapiService"; import ForbiddenRequestException from "../exceptions/forbiddenRequestException"; import WebapiCommandHandler from "../command/management/webapi/webapiCommandHandler"; diff --git a/src/data-source.ts b/src/data-source.ts index 37f957d..ed56dad 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import "reflect-metadata"; import { DataSource } from "typeorm"; -import { DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME, DB_TYPE, DB_PORT } from "./env.defaults"; +import { configCheck, DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_USERNAME } from "./env.defaults"; import { user } from "./entity/management/user"; import { refresh } from "./entity/refresh"; @@ -34,7 +34,7 @@ import { query } from "./entity/configuration/query"; import { memberView } from "./views/memberView"; import { memberExecutivePositionsView } from "./views/memberExecutivePositionView"; import { memberQualificationsView } from "./views/memberQualificationsView"; -import { membershipView } from "./views/membershipsView"; +import { membershipTotalView, membershipView } from "./views/membershipsView"; import { template } from "./entity/configuration/template"; import { templateUsage } from "./entity/configuration/templateUsage"; import { newsletter } from "./entity/club/newsletter/newsletter"; @@ -44,16 +44,17 @@ import { newsletterConfig } from "./entity/configuration/newsletterConfig"; import { webapi } from "./entity/management/webapi"; import { webapiPermission } from "./entity/management/webapi_permission"; import { salutation } from "./entity/configuration/salutation"; +import { setting } from "./entity/management/setting"; +import { education } from "./entity/configuration/education"; +import { memberEducations } from "./entity/club/member/memberEducations"; -import { BackupAndResetDatabase1738166124200 } from "./migrations/1738166124200-BackupAndResetDatabase"; -import { CreateSchema1738166167472 } from "./migrations/1738166167472-CreateSchema"; -import { TemplatesAndProtocolSort1742549956787 } from "./migrations/1742549956787-templatesAndProtocolSort"; -import { QueryToUUID1742922178643 } from "./migrations/1742922178643-queryToUUID"; -import { NewsletterColumnType1744351418751 } from "./migrations/1744351418751-newsletterColumnType"; -import { QueryUpdatedAt1744795756230 } from "./migrations/1744795756230-QueryUpdatedAt"; +import { BackupAndResetDatabase1749296262915 } from "./migrations/1749296262915-BackupAndResetDatabase"; +import { CreateSchema1749296280721 } from "./migrations/1749296280721-CreateSchema"; + +configCheck(); const dataSource = new DataSource({ - type: DB_TYPE as any, + type: "postgres", host: DB_HOST, port: DB_PORT, username: DB_USERNAME, @@ -61,7 +62,6 @@ const dataSource = new DataSource({ database: DB_NAME, synchronize: false, logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"], - bigNumberStrings: false, entities: [ user, refresh, @@ -75,6 +75,7 @@ const dataSource = new DataSource({ communicationType, executivePosition, membershipStatus, + education, qualification, salutation, member, @@ -82,6 +83,7 @@ const dataSource = new DataSource({ memberExecutivePositions, memberQualifications, membership, + memberEducations, protocol, protocolAgenda, protocolDecision, @@ -101,17 +103,12 @@ const dataSource = new DataSource({ memberExecutivePositionsView, memberQualificationsView, membershipView, + membershipTotalView, webapi, webapiPermission, + setting, ], - migrations: [ - BackupAndResetDatabase1738166124200, - CreateSchema1738166167472, - TemplatesAndProtocolSort1742549956787, - QueryToUUID1742922178643, - NewsletterColumnType1744351418751, - QueryUpdatedAt1744795756230, - ], + migrations: [BackupAndResetDatabase1749296262915, CreateSchema1749296280721], migrationsRun: true, migrationsTransactionMode: "each", subscribers: [], diff --git a/src/demodata/protocol.data.ts b/src/demodata/protocol.data.ts index 8d18b2a..437d280 100644 --- a/src/demodata/protocol.data.ts +++ b/src/demodata/protocol.data.ts @@ -7,7 +7,7 @@ export const protocolDemoData: { title: string; summary: string; iteration: number; - date: string; + date: Date; start: string; end: string; agenda: Array>; @@ -19,12 +19,7 @@ export const protocolDemoData: { title: "Beispiel Protokoll Daten", summary: "Zusammenfassung der Demodaten.", iteration: 1, - date: new Date().toLocaleDateString("de-DE", { - weekday: "long", - day: "2-digit", - month: "2-digit", - year: "numeric", - }), + date: new Date(), start: "19:00:00", end: "21:00:00", agenda: [ diff --git a/src/entity/club/calendar.ts b/src/entity/club/calendar.ts index 6e64553..a647e61 100644 --- a/src/entity/club/calendar.ts +++ b/src/entity/club/calendar.ts @@ -45,7 +45,7 @@ export class calendar { @UpdateDateColumn() updatedAt: Date; - @Column({ type: "varchar", nullable: true, default: null, unique: true }) + @Column({ type: "varchar", length: "255", nullable: true, default: null, unique: true }) webpageId: string; @ManyToOne(() => calendarType, (t) => t.calendar, { diff --git a/src/entity/club/member/member.ts b/src/entity/club/member/member.ts index 1678e39..0720f57 100644 --- a/src/entity/club/member/member.ts +++ b/src/entity/club/member/member.ts @@ -1,4 +1,15 @@ -import { Column, ColumnType, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, PrimaryColumn } from "typeorm"; +import { + Column, + ColumnType, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, + PrimaryColumn, + PrimaryGeneratedColumn, +} from "typeorm"; import { membership } from "./membership"; import { memberAwards } from "./memberAwards"; import { memberQualifications } from "./memberQualifications"; @@ -6,10 +17,11 @@ import { memberExecutivePositions } from "./memberExecutivePositions"; import { communication } from "./communication"; import { salutation } from "../../configuration/salutation"; import { getTypeByORM } from "../../../migrations/ormHelper"; +import { memberEducations } from "./memberEducations"; @Entity() export class member { - @PrimaryColumn({ generated: "uuid", type: "varchar" }) + @PrimaryGeneratedColumn("uuid") id: string; @Column({ type: "varchar", length: 255 }) @@ -27,9 +39,15 @@ export class member { @Column({ type: "varchar", length: 255, unique: true, nullable: true }) internalId?: string; + @Column({ type: "varchar", length: 255, nullable: true }) + note?: string; + @Column() salutationId: number; + @CreateDateColumn() + createdAt: Date; + @ManyToOne(() => salutation, (salutation) => salutation.members, { nullable: false, onDelete: "RESTRICT", @@ -53,6 +71,9 @@ export class member { @OneToMany(() => memberQualifications, (qualifications) => qualifications.member, { cascade: ["insert"] }) qualifications: memberQualifications[]; + @OneToMany(() => memberEducations, (educations) => educations.member, { cascade: ["insert"] }) + educations: memberEducations[]; + firstMembershipEntry?: membership; lastMembershipEntry?: membership; preferredCommunication?: Array; diff --git a/src/entity/club/member/memberEducations.ts b/src/entity/club/member/memberEducations.ts new file mode 100644 index 0000000..614d404 --- /dev/null +++ b/src/entity/club/member/memberEducations.ts @@ -0,0 +1,43 @@ +import { Column, ColumnType, Entity, ManyToOne, PrimaryColumn } from "typeorm"; +import { member } from "./member"; +import { education } from "../../configuration/education"; +import { getTypeByORM } from "../../../migrations/ormHelper"; + +@Entity() +export class memberEducations { + @PrimaryColumn({ generated: "increment", type: "int" }) + id: number; + + @Column({ type: getTypeByORM("date").type as ColumnType }) + start: Date; + + @Column({ type: getTypeByORM("date").type as ColumnType, nullable: true }) + end?: Date; + + @Column({ type: "varchar", length: 255, nullable: true }) + note?: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + place?: string; + + @Column() + memberId: string; + + @Column() + educationId: number; + + @ManyToOne(() => member, (member) => member.awards, { + nullable: false, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }) + member: member; + + @ManyToOne(() => education, (education) => education.members, { + nullable: false, + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + cascade: ["insert"], + }) + education: education; +} diff --git a/src/entity/club/newsletter/newsletter.ts b/src/entity/club/newsletter/newsletter.ts index 24258b4..ead7052 100644 --- a/src/entity/club/newsletter/newsletter.ts +++ b/src/entity/club/newsletter/newsletter.ts @@ -1,4 +1,4 @@ -import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm"; +import { Column, CreateDateColumn, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm"; import { newsletterDates } from "./newsletterDates"; import { newsletterRecipients } from "./newsletterRecipients"; import { query } from "../../configuration/query"; @@ -14,6 +14,9 @@ export class newsletter { @Column({ type: "varchar", length: 255, default: "" }) description: string; + @CreateDateColumn() + createdAt: Date; + @Column({ type: "text", default: "" }) newsletterTitle: string; diff --git a/src/entity/configuration/education.ts b/src/entity/configuration/education.ts new file mode 100644 index 0000000..e380570 --- /dev/null +++ b/src/entity/configuration/education.ts @@ -0,0 +1,17 @@ +import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm"; +import { memberEducations } from "../club/member/memberEducations"; + +@Entity() +export class education { + @PrimaryColumn({ generated: "increment", type: "int" }) + id: number; + + @Column({ type: "varchar", length: 255, unique: true }) + education: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + description?: string; + + @OneToMany(() => memberEducations, (memberEducations) => memberEducations.education) + members: memberEducations[]; +} diff --git a/src/entity/configuration/newsletterConfig.ts b/src/entity/configuration/newsletterConfig.ts index 17dde81..2d5d69c 100644 --- a/src/entity/configuration/newsletterConfig.ts +++ b/src/entity/configuration/newsletterConfig.ts @@ -1,5 +1,5 @@ import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; -import { NewsletterConfigType } from "../../enums/newsletterConfigType"; +import { NewsletterConfigEnum } from "../../enums/newsletterConfigEnum"; import { communicationType } from "./communicationType"; @Entity() @@ -11,15 +11,15 @@ export class newsletterConfig { type: "varchar", length: "255", transformer: { - to(value: NewsletterConfigType) { + to(value: NewsletterConfigEnum) { return value.toString(); }, from(value: string) { - return NewsletterConfigType[value as keyof typeof NewsletterConfigType]; + return NewsletterConfigEnum[value as keyof typeof NewsletterConfigEnum]; }, }, }) - config: NewsletterConfigType; + config: NewsletterConfigEnum; @ManyToOne(() => communicationType, { nullable: false, diff --git a/src/entity/configuration/query.ts b/src/entity/configuration/query.ts index 68f95be..419ad43 100644 --- a/src/entity/configuration/query.ts +++ b/src/entity/configuration/query.ts @@ -1,8 +1,8 @@ -import { Column, Entity, PrimaryColumn, UpdateDateColumn } from "typeorm"; +import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; @Entity() export class query { - @PrimaryColumn({ generated: "uuid", type: "varchar" }) + @PrimaryGeneratedColumn("uuid") id: string; @Column({ type: "varchar", length: 255, unique: true }) diff --git a/src/entity/management/setting.ts b/src/entity/management/setting.ts new file mode 100644 index 0000000..99d3cda --- /dev/null +++ b/src/entity/management/setting.ts @@ -0,0 +1,13 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity() +export class setting { + @PrimaryColumn({ type: "varchar", length: 255 }) + topic: string; + + @PrimaryColumn({ type: "varchar", length: 255 }) + key: string; + + @Column({ type: "text" }) + value: string; +} diff --git a/src/entity/management/user.ts b/src/entity/management/user.ts index 94ab3a3..27cd06a 100644 --- a/src/entity/management/user.ts +++ b/src/entity/management/user.ts @@ -1,10 +1,13 @@ -import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from "typeorm"; +import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm"; import { role } from "./role"; import { userPermission } from "./user_permission"; +import { LoginRoutineEnum } from "../../enums/loginRoutineEnum"; +import { CodingHelper } from "../../helpers/codingHelper"; +import { APPLICATION_SECRET } from "../../env.defaults"; @Entity() export class user { - @PrimaryColumn({ generated: "uuid", type: "varchar" }) + @PrimaryGeneratedColumn("uuid") id: string; @Column({ type: "varchar", unique: true, length: 255 }) @@ -19,11 +22,27 @@ export class user { @Column({ type: "varchar", length: 255 }) lastname: string; - @Column({ type: "varchar", length: 255 }) + @Column({ + type: "text", + select: false, + transformer: CodingHelper.entityBaseCoding(APPLICATION_SECRET, ""), + }) secret: string; - @Column({ type: "boolean", default: false }) - static: boolean; + @Column({ + type: "varchar", + length: "255", + default: LoginRoutineEnum.totp, + transformer: { + to(value: LoginRoutineEnum) { + return value.toString(); + }, + from(value: string) { + return LoginRoutineEnum[value as keyof typeof LoginRoutineEnum]; + }, + }, + }) + routine: LoginRoutineEnum; @Column({ type: "boolean", default: false }) isOwner: boolean; diff --git a/src/enums/loginRoutineEnum.ts b/src/enums/loginRoutineEnum.ts new file mode 100644 index 0000000..4d42334 --- /dev/null +++ b/src/enums/loginRoutineEnum.ts @@ -0,0 +1,4 @@ +export enum LoginRoutineEnum { + password = "password", // login with self defined password + totp = "totp", // login with totp by auth apps +} diff --git a/src/enums/newsletterConfigEnum.ts b/src/enums/newsletterConfigEnum.ts new file mode 100644 index 0000000..1e7313f --- /dev/null +++ b/src/enums/newsletterConfigEnum.ts @@ -0,0 +1,5 @@ +export enum NewsletterConfigEnum { + pdf = "pdf", + mail = "mail", + none = "none", +} diff --git a/src/enums/newsletterConfigType.ts b/src/enums/newsletterConfigType.ts deleted file mode 100644 index 4703494..0000000 --- a/src/enums/newsletterConfigType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum NewsletterConfigType { - pdf = "pdf", - mail = "mail", -} diff --git a/src/env.defaults.ts b/src/env.defaults.ts index 739fd87..7a408d6 100644 --- a/src/env.defaults.ts +++ b/src/env.defaults.ts @@ -2,32 +2,15 @@ import "dotenv/config"; import ms from "ms"; import ip from "ip"; -export const DB_TYPE = process.env.DB_TYPE ?? "mysql"; export const DB_HOST = process.env.DB_HOST ?? ""; -export const DB_PORT = Number(process.env.DB_PORT ?? 3306); +export const DB_PORT = Number(process.env.DB_PORT ?? 5432); export const DB_NAME = process.env.DB_NAME ?? ""; export const DB_USERNAME = process.env.DB_USERNAME ?? ""; export const DB_PASSWORD = process.env.DB_PASSWORD ?? ""; export const SERVER_PORT = Number(process.env.SERVER_PORT ?? 5000); -export const JWT_SECRET = process.env.JWT_SECRET ?? "my_jwt_secret_string_ilughfnadiuhgq§$IUZGFVRweiouarbt1oub3h5q4a"; -export const JWT_EXPIRATION = (process.env.JWT_EXPIRATION ?? "15m") as ms.StringValue; -export const REFRESH_EXPIRATION = (process.env.REFRESH_EXPIRATION ?? "1d") as ms.StringValue; -export const PWA_REFRESH_EXPIRATION = (process.env.PWA_REFRESH_EXPIRATION ?? "5d") as ms.StringValue; - -export const MAIL_USERNAME = process.env.MAIL_USERNAME ?? ""; -export const MAIL_PASSWORD = process.env.MAIL_PASSWORD ?? ""; -export const MAIL_HOST = process.env.MAIL_HOST ?? ""; -export const MAIL_PORT = Number(process.env.MAIL_PORT ?? "587"); -export const MAIL_SECURE = process.env.MAIL_SECURE ?? "false"; - -export const CLUB_NAME = process.env.CLUB_NAME ?? "FF Admin"; -export const CLUB_WEBSITE = process.env.CLUB_WEBSITE ?? ""; - -export const BACKUP_INTERVAL = Number(process.env.BACKUP_INTERVAL ?? "1"); -export const BACKUP_COPIES = Number(process.env.BACKUP_COPIES ?? "7"); -export const BACKUP_AUTO_RESTORE = process.env.BACKUP_AUTO_RESTORE ?? "true"; +export const APPLICATION_SECRET = process.env.APPLICATION_SECRET ?? ""; export const USE_SECURITY_STRICT_LIMIT = process.env.USE_SECURITY_STRICT_LIMIT ?? "true"; export const SECURITY_STRICT_LIMIT_WINDOW = (process.env.SECURITY_STRICT_LIMIT_WINDOW ?? "15m") as ms.StringValue; @@ -55,40 +38,15 @@ export const TRUST_PROXY = ((): Array | string | boolean | number | null })(); export function configCheck() { - if (DB_TYPE != "mysql" && DB_TYPE != "sqlite" && DB_TYPE != "postgres") - throw new Error("set valid value to DB_TYPE (mysql|sqlite|postgres)"); - if ((DB_HOST == "" || typeof DB_HOST != "string") && DB_TYPE != "sqlite") - throw new Error("set valid value to DB_HOST"); + if (DB_HOST == "" || typeof DB_HOST != "string") throw new Error("set valid value to DB_HOST"); if (DB_NAME == "" || typeof DB_NAME != "string") throw new Error("set valid value to DB_NAME"); - if ((DB_USERNAME == "" || typeof DB_USERNAME != "string") && DB_TYPE != "sqlite") - throw new Error("set valid value to DB_USERNAME"); - if ((DB_PASSWORD == "" || typeof DB_PASSWORD != "string") && DB_TYPE != "sqlite") - throw new Error("set valid value to DB_PASSWORD"); + if (DB_USERNAME == "" || typeof DB_USERNAME != "string") throw new Error("set valid value to DB_USERNAME"); + if (DB_PASSWORD == "" || typeof DB_PASSWORD != "string") throw new Error("set valid value to DB_PASSWORD"); + + if (APPLICATION_SECRET == "") throw new Error("set valid APPLICATION_SECRET"); if (isNaN(SERVER_PORT)) throw new Error("set valid numeric value to SERVER_PORT"); - if (JWT_SECRET == "" || typeof JWT_SECRET != "string") throw new Error("set valid value to JWT_SECRET"); - checkMS(JWT_EXPIRATION, "JWT_EXPIRATION"); - checkMS(REFRESH_EXPIRATION, "REFRESH_EXPIRATION"); - checkMS(PWA_REFRESH_EXPIRATION, "PWA_REFRESH_EXPIRATION"); - - if (MAIL_USERNAME == "" || typeof MAIL_USERNAME != "string") throw new Error("set valid value to MAIL_USERNAME"); - if (MAIL_PASSWORD == "" || typeof MAIL_PASSWORD != "string") throw new Error("set valid value to MAIL_PASSWORD"); - if (MAIL_HOST == "" || typeof MAIL_HOST != "string") throw new Error("set valid value to MAIL_HOST"); - if (isNaN(MAIL_PORT)) throw new Error("set valid numeric value to MAIL_PORT"); - if (MAIL_SECURE != "true" && MAIL_SECURE != "false") throw new Error("set 'true' or 'false' to MAIL_SECURE"); - - if ( - CLUB_WEBSITE != "" && - !/^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test(CLUB_WEBSITE) - ) - throw new Error("CLUB_WEBSITE is not valid url"); - - if (BACKUP_AUTO_RESTORE != "true" && BACKUP_AUTO_RESTORE != "false") - throw new Error("set 'true' or 'false' to BACKUP_AUTO_RESTORE"); - if (BACKUP_INTERVAL < 1) throw new Error("BACKUP_INTERVAL has to be at least 1"); - if (BACKUP_COPIES < 1) throw new Error("BACKUP_COPIES has to be at least 1"); - if (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false") throw new Error("set 'true' or 'false' to USE_SECURITY_STRICT_LIMIT"); checkMS(SECURITY_STRICT_LIMIT_WINDOW, "SECURITY_STRICT_LIMIT_WINDOW"); diff --git a/src/exceptions/databaseActionException.ts b/src/exceptions/databaseActionException.ts index b0b145c..eba9bb3 100644 --- a/src/exceptions/databaseActionException.ts +++ b/src/exceptions/databaseActionException.ts @@ -2,7 +2,7 @@ import CustomRequestException from "./customRequestException"; export default class DatabaseActionException extends CustomRequestException { constructor(action: string, table: string, err: any) { - let errstring = `${action} on ${table} with ${err?.code ?? "XX"} at ${err?.sqlMessage ?? "XX"}`; + let errstring = `${action} on ${table} with ${err?.code ?? "XX"} at ${err?.sqlMessage ?? err?.message ?? "XX"}`; super(500, errstring, err); } } diff --git a/src/factory/admin/club/member/dateMappingHelper.ts b/src/factory/admin/club/member/dateMappingHelper.ts index 8fdd072..40bab62 100644 --- a/src/factory/admin/club/member/dateMappingHelper.ts +++ b/src/factory/admin/club/member/dateMappingHelper.ts @@ -1,14 +1,5 @@ -import { DB_TYPE } from "../../../../env.defaults"; - export default abstract class DateMappingHelper { static mapDate(entry: any) { - switch (DB_TYPE) { - case "postgres": - return `${entry?.years ?? 0} years ${entry?.months ?? 0} months ${entry?.days ?? 0} days`; - case "mysql": - return entry.toString(); - case "sqlite": - return entry; - } + return `${entry?.years ?? 0} years ${entry?.months ?? 0} months ${entry?.days ?? 0} days`; } } diff --git a/src/factory/admin/club/member/member.ts b/src/factory/admin/club/member/member.ts index 246c0ae..9dd157f 100644 --- a/src/factory/admin/club/member/member.ts +++ b/src/factory/admin/club/member/member.ts @@ -20,7 +20,8 @@ export default abstract class MemberFactory { lastname: record?.lastname, nameaffix: record?.nameaffix, birthdate: record?.birthdate, - internalId: record.internalId, + internalId: record?.internalId, + note: record?.note, firstMembershipEntry: record?.firstMembershipEntry ? MembershipFactory.mapToSingle(record.firstMembershipEntry) : null, diff --git a/src/factory/admin/club/member/memberEducation.ts b/src/factory/admin/club/member/memberEducation.ts new file mode 100644 index 0000000..407100f --- /dev/null +++ b/src/factory/admin/club/member/memberEducation.ts @@ -0,0 +1,30 @@ +import { memberEducations } from "../../../../entity/club/member/memberEducations"; +import { MemberEducationViewModel } from "../../../../viewmodel/admin/club/member/memberEducation.models"; + +export default abstract class MemberEducationFactory { + /** + * @description map record to memberEducation + * @param {memberEducation} record + * @returns {MemberEducationViewModel} + */ + public static mapToSingle(record: memberEducations): MemberEducationViewModel { + return { + id: record.id, + start: record.start, + end: record.end, + note: record.note, + place: record.place, + education: record.education.education, + educationId: record.education.id, + }; + } + + /** + * @description map records to memberEducation + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/factory/admin/club/member/membership.ts b/src/factory/admin/club/member/membership.ts index b71d56c..a57063e 100644 --- a/src/factory/admin/club/member/membership.ts +++ b/src/factory/admin/club/member/membership.ts @@ -1,9 +1,10 @@ import { membership } from "../../../../entity/club/member/membership"; import { MembershipStatisticsViewModel, + MembershipTotalStatisticsViewModel, MembershipViewModel, } from "../../../../viewmodel/admin/club/member/membership.models"; -import { membershipView } from "../../../../views/membershipsView"; +import { membershipTotalView, membershipView } from "../../../../views/membershipsView"; import DateMappingHelper from "./dateMappingHelper"; export default abstract class MembershipFactory { @@ -53,6 +54,25 @@ export default abstract class MembershipFactory { }; } + /** + * @description map view record to MembershipTotalStatisticsViewModel + * @param {membershipTotalView} record + * @returns {MembershipTotalStatisticsViewModel} + */ + public static mapToSingleTotalStatistic(record: membershipTotalView): MembershipTotalStatisticsViewModel { + return { + durationInDays: record.durationInDays, + durationInYears: record.durationInYears, + exactDuration: DateMappingHelper.mapDate(record.exactDuration), + memberId: record.memberId, + memberSalutation: record.memberSalutation, + memberFirstname: record.memberFirstname, + memberLastname: record.memberLastname, + memberNameaffix: record.memberNameaffix, + memberBirthdate: record.memberBirthdate, + }; + } + /** * @description map records to MembershipStatisticsViewModel * @param {Array} records diff --git a/src/factory/admin/club/newsletter/newsletter.ts b/src/factory/admin/club/newsletter/newsletter.ts index 39c19e3..f47b6eb 100644 --- a/src/factory/admin/club/newsletter/newsletter.ts +++ b/src/factory/admin/club/newsletter/newsletter.ts @@ -18,7 +18,7 @@ export default abstract class NewsletterFactory { newsletterSignatur: record.newsletterSignatur, isSent: record.isSent, recipientsByQueryId: record?.recipientsByQuery ? record.recipientsByQuery.id : null, - recipientsByQuery: record?.recipientsByQuery ? QueryStoreFactory.mapToSingle(record.recipientsByQuery) : null, + createdAt: record.createdAt, }; } diff --git a/src/factory/admin/configuration/education.ts b/src/factory/admin/configuration/education.ts new file mode 100644 index 0000000..c130a6e --- /dev/null +++ b/src/factory/admin/configuration/education.ts @@ -0,0 +1,26 @@ +import { education } from "../../../entity/configuration/education"; +import { EducationViewModel } from "../../../viewmodel/admin/configuration/education.models"; + +export default abstract class EducationFactory { + /** + * @description map record to education + * @param {education} record + * @returns {AwardViewModel} + */ + public static mapToSingle(record: education): EducationViewModel { + return { + id: record.id, + education: record.education, + description: record.description, + }; + } + + /** + * @description map records to education + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/factory/admin/configuration/queryStore.ts b/src/factory/admin/configuration/queryStore.ts index 5f7b8a6..48ab793 100644 --- a/src/factory/admin/configuration/queryStore.ts +++ b/src/factory/admin/configuration/queryStore.ts @@ -12,6 +12,7 @@ export default abstract class QueryStoreFactory { id: record.id, title: record.title, query: record.query.startsWith("{") ? JSON.parse(record.query) : record.query, + updatedAt: record.updatedAt, }; } diff --git a/src/handlebars.config.ts b/src/handlebars.config.ts index 2cbded1..19b32ad 100644 --- a/src/handlebars.config.ts +++ b/src/handlebars.config.ts @@ -8,6 +8,14 @@ Handlebars.registerHelper("date", function (aString) { }); }); +Handlebars.registerHelper("weekdayDayMonth", function (aString) { + return new Date(aString).toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "long", + }); +}); + Handlebars.registerHelper("longdate", function (aString) { return new Date(aString).toLocaleDateString("de-DE", { weekday: "long", @@ -27,6 +35,27 @@ Handlebars.registerHelper("datetime", function (aString) { }); }); +Handlebars.registerHelper("longdatetime", function (aString) { + return new Date(aString).toLocaleDateString("de-DE", { + day: "2-digit", + month: "long", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +}); + +Handlebars.registerHelper("longdatetimeWithWeekday", function (aString) { + return new Date(aString).toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "long", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +}); + Handlebars.registerHelper("json", function (context) { return JSON.stringify(context); }); diff --git a/src/helpers/backupHelper.ts b/src/helpers/backupHelper.ts index 8a6191f..2a4f678 100644 --- a/src/helpers/backupHelper.ts +++ b/src/helpers/backupHelper.ts @@ -4,9 +4,10 @@ import { EntityManager } from "typeorm"; import uniqBy from "lodash.uniqby"; import InternalException from "../exceptions/internalException"; import UserService from "../service/management/userService"; -import { BACKUP_COPIES, BACKUP_INTERVAL } from "../env.defaults"; import DatabaseActionException from "../exceptions/databaseActionException"; import { availableTemplates } from "../type/templateTypes"; +import SettingHelper from "./settingsHelper"; +import { LoginRoutineEnum } from "../enums/loginRoutineEnum"; export type BackupSection = | "member" @@ -18,7 +19,8 @@ export type BackupSection = | "query" | "template" | "user" - | "webapi"; + | "webapi" + | "settings"; export type BackupSectionRefered = { [key in BackupSection]?: Array; @@ -42,6 +44,7 @@ export default abstract class BackupHelper { { type: "template", orderOnInsert: 2, orderOnClear: 1 }, // INSERT depends on member com { type: "user", orderOnInsert: 1, orderOnClear: 1 }, { type: "webapi", orderOnInsert: 1, orderOnClear: 1 }, + { type: "settings", orderOnInsert: 1, orderOnClear: 1 }, ]; private static readonly backupSectionRefered: BackupSectionRefered = { @@ -52,6 +55,7 @@ export default abstract class BackupHelper { "member_executive_positions", "membership", "communication", + "member_educations", ], memberBase: [ "award", @@ -60,6 +64,7 @@ export default abstract class BackupHelper { "membership_status", "communication_type", "salutation", + "education", ], protocol: [ "protocol", @@ -76,6 +81,7 @@ export default abstract class BackupHelper { template: ["template", "template_usage"], user: ["user", "user_permission", "role", "role_permission", "invite"], webapi: ["webapi", "webapi_permission"], + settings: ["setting"], }; private static transactionManager: EntityManager; @@ -103,7 +109,7 @@ export default abstract class BackupHelper { let files = FileSystemHelper.getFilesInDirectory("backup", ".json"); let sorted = files.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime()); - const filesToDelete = sorted.slice(BACKUP_COPIES); + const filesToDelete = sorted.slice(SettingHelper.getSetting("backup.copies")); for (const file of filesToDelete) { FileSystemHelper.deleteFile("backup", file); } @@ -117,7 +123,7 @@ export default abstract class BackupHelper { let diffInMs = new Date().getTime() - lastBackup.getTime(); let diffInDays = diffInMs / (1000 * 60 * 60 * 24); - if (diffInDays >= BACKUP_INTERVAL) { + if (diffInDays >= SettingHelper.getSetting("backup.interval")) { await this.createBackup({}); } } @@ -155,7 +161,7 @@ export default abstract class BackupHelper { for (const section of sections.filter((s) => Object.keys(backup).includes(s.type))) { let refered = this.backupSectionRefered[section.type]; for (const ref of refered) { - await this.transactionManager.getRepository(ref).delete({}); + await this.transactionManager.getRepository(ref).deleteAll(); } } } @@ -220,6 +226,8 @@ export default abstract class BackupHelper { return await this.getUser(collectIds); case "webapi": return await this.getWebapi(); + case "settings": + return await this.getSettings(); default: return []; } @@ -240,6 +248,8 @@ export default abstract class BackupHelper { .leftJoin("positions.executivePosition", "executivePosition") .leftJoin("member.qualifications", "qualifications") .leftJoin("qualifications.qualification", "qualification") + .leftJoin("member.educations", "educations") + .leftJoin("educations.education", "education") .select([ ...(collectIds ? ["member.id"] : []), "member.firstname", @@ -247,6 +257,7 @@ export default abstract class BackupHelper { "member.nameaffix", "member.birthdate", "member.internalId", + "member.note", ]) .addSelect(["salutation.salutation"]) .addSelect([ @@ -274,6 +285,14 @@ export default abstract class BackupHelper { "qualification.qualification", "qualification.description", ]) + .addSelect([ + "educations.start", + "educations.end", + "educations.place", + "educations.note", + "education.education", + "education.description", + ]) .getMany(); } private static async getMemberBase(): Promise<{ [key: string]: Array }> { @@ -288,6 +307,7 @@ export default abstract class BackupHelper { qualification: await dataSource .getRepository("qualification") .find({ select: { qualification: true, description: true } }), + education: await dataSource.getRepository("education").find({ select: { education: true, description: true } }), }; } private static async getProtocol(collectIds: boolean): Promise> { @@ -338,6 +358,7 @@ export default abstract class BackupHelper { "newsletter.newsletterText", "newsletter.newsletterSignatur", "newsletter.isSent", + "newsletter.createdAt", ]) .addSelect(["dates.calendarId", "dates.diffTitle", "dates.diffDescription"]) .addSelect(["recipients.memberId"]) @@ -349,7 +370,12 @@ export default abstract class BackupHelper { "member.birthdate", "member.internalId", ]) - .addSelect([...(collectIds ? ["query.id"] : []), "recipientsByQuery.title", "recipientsByQuery.query"]) + .addSelect([ + ...(collectIds ? ["recipientsByQuery.id"] : []), + "recipientsByQuery.title", + "recipientsByQuery.query", + "recipientsByQuery.updatedAt", + ]) .getMany() .then((res: any) => res.map((n: any) => ({ @@ -430,6 +456,7 @@ export default abstract class BackupHelper { "user.firstname", "user.lastname", "user.secret", + "user.routine", "user.isOwner", ]) .addSelect(["permissions.permission"]) @@ -455,6 +482,13 @@ export default abstract class BackupHelper { .addSelect(["permissions.permission"]) .getMany(); } + private static async getSettings(): Promise> { + return await dataSource + .getRepository("setting") + .createQueryBuilder("setting") + .select(["setting.topic", "setting.key", "setting.value"]) + .getMany(); + } private static async setSectionData( section: BackupSection, @@ -471,6 +505,7 @@ export default abstract class BackupHelper { if (section == "template" && !Array.isArray(data)) await this.setTemplate(data); if (section == "user" && !Array.isArray(data)) await this.setUser(data); if (section == "webapi" && Array.isArray(data)) await this.setWebapi(data); + if (section == "settings" && Array.isArray(data)) await this.setSettings(data); } private static async setMemberData(data: Array): Promise { @@ -514,6 +549,13 @@ export default abstract class BackupHelper { .map((d) => ({ ...d, id: undefined })), "qualification" ), + education: uniqBy( + data + .map((d) => (d.education ?? []).map((c: any) => c.education)) + .flat() + .map((d) => ({ ...d, id: undefined })), + "education" + ), }); let salutation = await this.transactionManager.getRepository("salutation").find(); @@ -521,6 +563,7 @@ export default abstract class BackupHelper { let membership = await this.transactionManager.getRepository("membership_status").find(); let award = await this.transactionManager.getRepository("award").find(); let qualification = await this.transactionManager.getRepository("qualification").find(); + let education = await this.transactionManager.getRepository("education").find(); let position = await this.transactionManager.getRepository("executive_position").find(); let dataWithMappedIds = data.map((d) => ({ ...d, @@ -563,6 +606,13 @@ export default abstract class BackupHelper { id: qualification.find((iq) => iq.qualification == q.qualification.qualification)?.id ?? undefined, }, })), + educations: (d.educations ?? []).map((e: any) => ({ + ...e, + education: { + ...e.education, + id: education.find((id) => id.education == e.education.education)?.id ?? undefined, + }, + })), })); await this.transactionManager.getRepository("member").save(dataWithMappedIds); } @@ -573,6 +623,7 @@ export default abstract class BackupHelper { let award = await this.transactionManager.getRepository("award").find(); let qualification = await this.transactionManager.getRepository("qualification").find(); let position = await this.transactionManager.getRepository("executive_position").find(); + let education = await this.transactionManager.getRepository("education").find(); await this.transactionManager .createQueryBuilder() @@ -614,10 +665,19 @@ export default abstract class BackupHelper { .insert() .into("qualification") .values( - (data?.["qualification"] ?? []).filter((d) => !qualification.map((q) => q.award).includes(d.qualification)) + (data?.["qualification"] ?? []).filter( + (d) => !qualification.map((q) => q.qualification).includes(d.qualification) + ) ) .orIgnore() .execute(); + await this.transactionManager + .createQueryBuilder() + .insert() + .into("education") + .values((data?.["education"] ?? []).filter((d) => !education.map((q) => q.education).includes(d.education))) + .orIgnore() + .execute(); } private static async setProtocol(data: Array, collectedIds: boolean): Promise { let members = await this.transactionManager.getRepository("member").find(); @@ -750,11 +810,11 @@ export default abstract class BackupHelper { .filter((d) => availableTemplates.includes(d.scope)) .map((d) => ({ ...d, - headerHeightId: templates.find((template) => template.template == d.headerHeight.template)?.id ?? null, - footerHeightId: templates.find((template) => template.template == d.footerHeight.template)?.id ?? null, - headerId: templates.find((template) => template.template == d.header.template)?.id ?? null, - bodyId: templates.find((template) => template.template == d.body.template)?.id ?? null, - footerId: templates.find((template) => template.template == d.footer.template)?.id ?? null, + headerHeightId: templates.find((template) => template.template == d.headerHeight)?.id ?? null, + footerHeightId: templates.find((template) => template.template == d.footerHeight)?.id ?? null, + headerId: templates.find((template) => template.template == d.header?.template)?.id ?? null, + bodyId: templates.find((template) => template.template == d.body?.template)?.id ?? null, + footerId: templates.find((template) => template.template == d.footer?.template)?.id ?? null, })); availableTemplates.forEach((at) => { if (!dataWithMappedId.some((d) => d.scope == at)) { @@ -788,6 +848,7 @@ export default abstract class BackupHelper { let roles = await this.transactionManager.getRepository("role").find(); let dataWithMappedIds = (data?.["user"] ?? []).map((u) => ({ ...u, + routine: u.routine ?? LoginRoutineEnum.totp, roles: u.roles.map((r: any) => ({ ...r, id: roles.find((role) => role.role == r.role)?.id ?? undefined, @@ -805,4 +866,7 @@ export default abstract class BackupHelper { private static async setWebapi(data: Array): Promise { await this.transactionManager.getRepository("webapi").save(data); } + private static async setSettings(data: Array): Promise { + await this.transactionManager.getRepository("setting").save(data); + } } diff --git a/src/helpers/calendarHelper.ts b/src/helpers/calendarHelper.ts index 7418089..e10e337 100644 --- a/src/helpers/calendarHelper.ts +++ b/src/helpers/calendarHelper.ts @@ -1,7 +1,7 @@ import { createEvents } from "ics"; import { calendar } from "../entity/club/calendar"; import moment from "moment"; -import { CLUB_NAME, CLUB_WEBSITE, MAIL_USERNAME } from "../env.defaults"; +import SettingHelper from "./settingsHelper"; export abstract class CalendarHelper { public static buildICS(entries: Array): { error?: Error; value?: string } { @@ -35,7 +35,10 @@ export abstract class CalendarHelper { description: i.content, location: i.location, categories: [i.type.type], - organizer: { name: CLUB_NAME, email: MAIL_USERNAME }, + organizer: { + name: SettingHelper.getSetting("club.name"), + email: SettingHelper.getSetting("mail.username"), + }, created: moment(i.createdAt) .format("YYYY-M-D-H-m") .split("-") @@ -46,7 +49,7 @@ export abstract class CalendarHelper { .map((a) => parseInt(a)) as [number, number, number, number, number], transp: "OPAQUE" as "OPAQUE", status: "CONFIRMED", - ...(CLUB_WEBSITE != "" ? { url: CLUB_WEBSITE } : {}), + ...(SettingHelper.getSetting("club.website") != "" ? { url: SettingHelper.getSetting("club.website") } : {}), alarms: [ { action: "display", diff --git a/src/helpers/codingHelper.ts b/src/helpers/codingHelper.ts new file mode 100644 index 0000000..fee835a --- /dev/null +++ b/src/helpers/codingHelper.ts @@ -0,0 +1,95 @@ +import { createCipheriv, createDecipheriv, scryptSync, randomBytes } from "crypto"; +import { ValueTransformer } from "typeorm"; + +export abstract class CodingHelper { + private static readonly algorithm = "aes-256-gcm"; + private static readonly ivLength = 16; + private static readonly authTagLength = 16; + + static entityBaseCoding(key: string = "", fallback: string = ""): ValueTransformer { + return { + from(val: string | null | undefined): string { + if (!val || val == "") return fallback; + try { + return CodingHelper.decrypt(key, val, true); + } catch (error) { + console.error("Decryption error in database-read - can be ignored"); + if (fallback == "") return val; + else return fallback; + } + }, + to(val: string | null | undefined): string { + const valueToEncrypt = val || fallback; + if (valueToEncrypt === "") return ""; + + try { + return CodingHelper.encrypt(key, valueToEncrypt, true); + } catch (error) { + console.error("Encryption error in database-read - can be ignored"); + if (fallback == "") return val; + return ""; + } + }, + }; + } + + public static encrypt(phrase: string, content: string, passError = false): string { + if (!content) return ""; + + try { + // Generiere zufälligen IV für jede Verschlüsselung (sicherer als statischer IV) + const iv = randomBytes(this.ivLength); + const key = scryptSync(phrase, "salt", 32); + + const cipher = createCipheriv(this.algorithm, Uint8Array.from(key), Uint8Array.from(iv)); + + // Verschlüssele den Inhalt + let encrypted = cipher.update(content, "utf8", "hex"); + encrypted += cipher.final("hex"); + + // Speichere das Auth-Tag für GCM (wichtig für die Entschlüsselung) + const authTag = cipher.getAuthTag(); + + // Gib das Format: iv:verschlüsselter_text:authTag zurück + return Buffer.concat([ + Uint8Array.from(iv), + Uint8Array.from(Buffer.from(encrypted, "hex")), + Uint8Array.from(authTag), + ]).toString("base64"); + } catch (error) { + if (passError) throw error; + console.error("Encryption failed:", error); + return ""; + } + } + + public static decrypt(phrase: string, content: string, passError = false): string { + if (!content) return ""; + + try { + // Dekodiere den Base64-String + const buffer = Buffer.from(content, "base64"); + + // Extrahiere IV, verschlüsselten Text und Auth-Tag + const iv = buffer.subarray(0, this.ivLength); + const authTag = buffer.subarray(buffer.length - this.authTagLength); + const encryptedText = buffer.subarray(this.ivLength, buffer.length - this.authTagLength).toString("hex"); + + const key = scryptSync(phrase, "salt", 32); + + // Erstelle Decipher und setze Auth-Tag + const decipher = createDecipheriv(this.algorithm, Uint8Array.from(key), Uint8Array.from(iv)); + decipher.setAuthTag(Uint8Array.from(authTag)); + + // Entschlüssele den Text + let decrypted = decipher.update(encryptedText, "hex", "utf8"); + decrypted += decipher.final("utf8"); + + return decrypted; + } catch (error) { + if (passError) throw error; + console.error("Decryption failed:", error); + return ""; + } + } +} diff --git a/src/helpers/convertHelper.ts b/src/helpers/convertHelper.ts new file mode 100644 index 0000000..d797126 --- /dev/null +++ b/src/helpers/convertHelper.ts @@ -0,0 +1,83 @@ +import ms from "ms"; +import validator from "validator"; + +export abstract class TypeConverter { + abstract fromString(value: string): T; + abstract toString(value: T): string; + abstract validate(value: string): boolean; +} + +export abstract class StringTypeConverter extends TypeConverter { + fromString(value: string): string { + return value; + } + toString(value: string): string { + return value; + } + validate(value: string): boolean { + return typeof value === "string"; + } +} + +export abstract class NumberTypeConverter extends TypeConverter { + fromString(value: string): number { + return Number(value); + } + toString(value: number): string { + return String(value); + } + validate(value: string): boolean { + const num = Number(value); + return !isNaN(num); + } +} + +export abstract class BooleanTypeConverter extends TypeConverter { + fromString(value: string): boolean { + return value === "true"; + } + toString(value: boolean): string { + return value ? "true" : "false"; + } + validate(value: string): boolean { + return value === "true" || value === "false"; + } +} + +export abstract class MsTypeConverter extends TypeConverter { + fromString(value: string): ms.StringValue { + return value as ms.StringValue; + } + toString(value: ms.StringValue): string { + return String(value); + } + validate(value: string): boolean { + try { + const result = ms(value as ms.StringValue); + return result !== undefined; + } catch (e) { + return false; + } + } +} + +export abstract class EmailTypeConverter extends TypeConverter { + fromString(value: string): string { + return value; + } + toString(value: string): string { + return value; + } + validate(value: string): boolean { + return validator.isEmail(value); + } +} + +// Konkrete Implementierungen der Converter +export class StringConverter extends StringTypeConverter {} +export class LongStringConverter extends StringTypeConverter {} +export class UrlConverter extends StringTypeConverter {} +export class NumberConverter extends NumberTypeConverter {} +export class BooleanConverter extends BooleanTypeConverter {} +export class MsConverter extends MsTypeConverter {} +export class EmailConverter extends EmailTypeConverter {} diff --git a/src/helpers/demoDataHelper.ts b/src/helpers/demoDataHelper.ts index b577eb7..d5fd27e 100644 --- a/src/helpers/demoDataHelper.ts +++ b/src/helpers/demoDataHelper.ts @@ -12,6 +12,16 @@ export abstract class DemoDataHelper { return newsletterDemoData; case "member": return memberDemoData; + case "listprint": + return { + today: new Date(), + list: [memberDemoData.memberships], + }; + case "listprint.member": + return { + today: new Date(), + list: [memberDemoData.member], + }; default: return {}; } diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index 580bbd2..25d7554 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -1,4 +1,4 @@ -import { Brackets, DataSource, NotBrackets, ObjectLiteral, SelectQueryBuilder, WhereExpressionBuilder } from "typeorm"; +import { Brackets, NotBrackets, ObjectLiteral, SelectQueryBuilder, WhereExpressionBuilder } from "typeorm"; import { dataSource } from "../data-source"; import { ConditionStructure, DynamicQueryStructure, FieldType, QueryResult } from "../type/dynamicQueries"; import { TableMeta } from "../type/tableMeta"; @@ -17,11 +17,13 @@ export default abstract class DynamicQueryBuilder { "memberAwards", "memberExecutivePositions", "memberQualifications", + "memberEducations", "membership", "memberView", "memberExecutivePositionsView", "memberQualificationsView", "membershipView", + "membershipTotalView", ]; public static getTableMeta(tableName: string): TableMeta { @@ -63,7 +65,7 @@ export default abstract class DynamicQueryBuilder { count?: number; noLimit?: boolean; }): SelectQueryBuilder { - let affix = queryObj.id ?? StringHelper.random(10); + let affix = queryObj.id.replaceAll("-", "") ?? StringHelper.random(10); let query = dataSource.getRepository(queryObj.table).createQueryBuilder(`${affix}_${queryObj.table}`); this.buildDynamicQuery(query, queryObj, affix); @@ -116,7 +118,7 @@ export default abstract class DynamicQueryBuilder { if (queryObject.join) { for (const join of queryObject.join) { - let subaffix = join.id ?? StringHelper.random(10); + let subaffix = join.id.replaceAll("-", "") ?? StringHelper.random(10); if (join.type == undefined) join.type = "defined"; if (join.type == "defined") { query.innerJoin(`${alias}.${join.foreignColumn}`, `${subaffix}_${join.table}`); @@ -226,19 +228,19 @@ export default abstract class DynamicQueryBuilder { query += ` IS NOT NULL`; break; case "contains": - query += ` LIKE :${parameterKey}`; + query += ` ILIKE :${parameterKey}`; parameters[parameterKey] = `%${condition.value}%`; break; case "notContains": - query += ` NOT LIKE :${parameterKey}`; + query += ` NOT ILIKE :${parameterKey}`; parameters[parameterKey] = `%${condition.value}%`; break; case "startsWith": - query += ` LIKE :${parameterKey}`; + query += ` ILIKE :${parameterKey}`; parameters[parameterKey] = `${condition.value}%`; break; case "endsWith": - query += ` LIKE :${parameterKey}`; + query += ` ILIKE :${parameterKey}`; parameters[parameterKey] = `%${condition.value}`; break; case "timespanEq": @@ -272,8 +274,15 @@ export default abstract class DynamicQueryBuilder { }); }); results = tempResults; - } else if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) { - const objResults = flatten(value as QueryResult, newKey); + } else if ( + value && + typeof value === "object" && + !Array.isArray(value) && + !(value instanceof Date) && + !(value instanceof Buffer) && + !Object.keys(value).every((k) => ["years", "months", "days"].includes(k)) + ) { + const objResults = flatten(value, newKey); const tempResults: Array<{ [key: string]: FieldType }> = []; results.forEach((res) => { objResults.forEach((objRes) => { @@ -283,7 +292,24 @@ export default abstract class DynamicQueryBuilder { results = tempResults; } else { results.forEach((res) => { - if (String(value) != "undefined") res[newKey] = String(value); + if (typeof value === "object" && value instanceof Date) { + res[newKey] = new Date(value).toISOString(); + } else if ( + typeof value === "object" && + !Array.isArray(value) && + !(value instanceof Buffer) && + value !== null + ) { + let string = ""; + for (const key of Object.keys(value)) { + string += `${value[key]} ${key} `; + } + res[newKey] = string.trim(); + + // JSON.stringify(value).replace(/["\\{}]/g, "").replaceAll(",", ", "); + } else if (String(value) != "undefined") { + res[newKey] = value !== null ? String(value) : ""; + } }); } } @@ -366,7 +392,7 @@ export default abstract class DynamicQueryBuilder { stats: "error", sql: error.sql, code: error.code, - msg: error.sqlMessage, + msg: error.sqlMessage ?? error.message, }; } }); @@ -375,7 +401,7 @@ export default abstract class DynamicQueryBuilder { stats: "error", sql: error.sql, code: error.code, - msg: error.sqlMessage, + msg: error.sqlMessage ?? error.message, }; } } else { @@ -395,7 +421,7 @@ export default abstract class DynamicQueryBuilder { stats: "error", sql: error.sql, code: error.code, - msg: error.sqlMessage, + msg: error.sqlMessage ?? error.message, }; } } diff --git a/src/helpers/fileSystemHelper.ts b/src/helpers/fileSystemHelper.ts index f77bef2..9bc409a 100644 --- a/src/helpers/fileSystemHelper.ts +++ b/src/helpers/fileSystemHelper.ts @@ -20,9 +20,20 @@ export abstract class FileSystemHelper { return readFileSync(this.formatPath(...filePath), "base64"); } + static readRootFile(filePath: string) { + return readFileSync(this.normalizePath(process.cwd(), filePath), "utf8"); + } + static readTemplateFile(filePath: string) { - this.createFolder(filePath); - return readFileSync(process.cwd() + filePath, "utf8"); + return readFileSync(this.normalizePath(process.cwd(), "src", "templates", filePath), "utf8"); + } + + static readAssetFile(filePath: string, returnPath: boolean = false) { + let path = this.normalizePath(process.cwd(), "src", "assets", filePath); + if (returnPath) { + return path; + } + return readFileSync(path, "utf8"); } static writeFile(filePath: string, filename: string, file: any) { diff --git a/src/helpers/jwtHelper.ts b/src/helpers/jwtHelper.ts index 5708ab8..066bb8b 100644 --- a/src/helpers/jwtHelper.ts +++ b/src/helpers/jwtHelper.ts @@ -1,6 +1,5 @@ import jwt from "jsonwebtoken"; import { JWTData, JWTToken } from "../type/jwtTypes"; -import { JWT_SECRET, JWT_EXPIRATION } from "../env.defaults"; import InternalException from "../exceptions/internalException"; import RolePermissionService from "../service/management/rolePermissionService"; import UserPermissionService from "../service/management/userPermissionService"; @@ -9,11 +8,13 @@ import PermissionHelper from "./permissionHelper"; import WebapiService from "../service/management/webapiService"; import WebapiPermissionService from "../service/management/webapiPermissionService"; import ms from "ms"; +import SettingHelper from "./settingsHelper"; +import { APPLICATION_SECRET } from "../env.defaults"; export abstract class JWTHelper { static validate(token: string): Promise { return new Promise((resolve, reject) => { - jwt.verify(token, JWT_SECRET, (err, decoded) => { + jwt.verify(token, APPLICATION_SECRET, (err, decoded) => { if (err) reject(err.message); else resolve(decoded); }); @@ -27,9 +28,11 @@ export abstract class JWTHelper { return new Promise((resolve, reject) => { jwt.sign( data, - JWT_SECRET, + APPLICATION_SECRET, { - ...(useExpiration ?? true ? { expiresIn: expOverwrite ?? JWT_EXPIRATION } : {}), + ...(useExpiration ?? true + ? { expiresIn: expOverwrite ?? (SettingHelper.getSetting("session.jwt_expiration") as ms.StringValue) } + : {}), }, (err, token) => { if (err) reject(err.message); @@ -100,7 +103,8 @@ export abstract class JWTHelper { }; let overwriteExpiration = - ms(JWT_EXPIRATION) < new Date().getTime() - new Date(expiration).getTime() + ms(SettingHelper.getSetting("session.jwt_expiration") as ms.StringValue) < + new Date().getTime() - new Date(expiration).getTime() ? null : Date.now() - new Date(expiration).getTime(); diff --git a/src/helpers/mailHelper.ts b/src/helpers/mailHelper.ts index ab44a26..ff778b9 100644 --- a/src/helpers/mailHelper.ts +++ b/src/helpers/mailHelper.ts @@ -1,17 +1,78 @@ import { Transporter, createTransport, TransportOptions } from "nodemailer"; -import { CLUB_NAME, MAIL_HOST, MAIL_PASSWORD, MAIL_PORT, MAIL_SECURE, MAIL_USERNAME } from "../env.defaults"; import { Attachment } from "nodemailer/lib/mailer"; +import SettingHelper from "./settingsHelper"; +import validator from "validator"; export default abstract class MailHelper { - private static readonly transporter: Transporter = createTransport({ - host: MAIL_HOST, - port: MAIL_PORT, - secure: (MAIL_SECURE as "true" | "false") == "true", - auth: { - user: MAIL_USERNAME, - pass: MAIL_PASSWORD, - }, - } as TransportOptions); + private static transporter: Transporter; + + static createTransport() { + this.transporter?.close(); + + this.transporter = createTransport({ + host: SettingHelper.getSetting("mail.host"), + port: SettingHelper.getSetting("mail.port"), + secure: SettingHelper.getSetting("mail.secure"), + auth: { + user: SettingHelper.getSetting("mail.username"), + pass: SettingHelper.getSetting("mail.password"), + }, + } as TransportOptions); + } + + static async verifyTransport({ + host, + port, + secure, + user, + password, + }: { + host: string; + port: number; + secure: boolean; + user: string; + password: string; + }): Promise { + let transport = createTransport({ + host, + port, + secure, + auth: { user, pass: password }, + }); + + return await transport + .verify() + .then(() => { + return true; + }) + .catch((err) => { + console.log(err); + return false; + }) + .finally(() => { + try { + transport?.close(); + } catch (error) {} + }); + } + + static async checkMail(mail: string): Promise { + return validator.isEmail(mail); + // return await emailCheck(mail) + // .then((res) => { + // return res; + // }) + // .catch((err) => { + // return false; + // }); + } + + static initialize() { + SettingHelper.onSettingTopicChanged("mail", () => { + this.createTransport(); + }); + this.createTransport(); + } /** * @description send mail @@ -29,7 +90,7 @@ export default abstract class MailHelper { return new Promise((resolve, reject) => { this.transporter .sendMail({ - from: `"${CLUB_NAME}" <${MAIL_USERNAME}>`, + from: `"${SettingHelper.getSetting("club.name")}" <${SettingHelper.getSetting("mail.email")}>`, to: target, subject, text: content, diff --git a/src/helpers/newsletterHelper.ts b/src/helpers/newsletterHelper.ts index e84427f..26f9342 100644 --- a/src/helpers/newsletterHelper.ts +++ b/src/helpers/newsletterHelper.ts @@ -10,13 +10,13 @@ import { CalendarHelper } from "./calendarHelper"; import DynamicQueryBuilder from "./dynamicQueryBuilder"; import { FileSystemHelper } from "./fileSystemHelper"; import MailHelper from "./mailHelper"; -import { CLUB_NAME } from "../env.defaults"; import { TemplateHelper } from "./templateHelper"; import { PdfExport } from "./pdfExport"; import NewsletterConfigService from "../service/configuration/newsletterConfigService"; -import { NewsletterConfigType } from "../enums/newsletterConfigType"; +import { NewsletterConfigEnum } from "../enums/newsletterConfigEnum"; import InternalException from "../exceptions/internalException"; import EventEmitter from "events"; +import SettingHelper from "./settingsHelper"; export interface NewsletterEventType { kind: "pdf" | "mail"; @@ -66,33 +66,7 @@ export abstract class NewsletterHelper { title: d.diffTitle || d.calendar.title, content: d.diffDescription || d.calendar.content, starttime: d.calendar.starttime, - formattedStarttime: new Date(d.calendar.starttime).toLocaleDateString("de-DE", { - weekday: "long", - day: "2-digit", - month: "long", - }), - formattedFullStarttime: new Date(d.calendar.starttime).toLocaleDateString("de-DE", { - weekday: "long", - day: "2-digit", - month: "long", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }), endtime: d.calendar.endtime, - formattedEndtime: new Date(d.calendar.endtime).toLocaleDateString("de-DE", { - weekday: "long", - day: "2-digit", - month: "long", - }), - formattedFullEndtime: new Date(d.calendar.endtime).toLocaleDateString("de-DE", { - weekday: "long", - day: "2-digit", - month: "long", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }), location: d.calendar.location, })) .sort((a, b) => a.starttime.getTime() - b.starttime.getTime()), @@ -145,7 +119,7 @@ export abstract class NewsletterHelper { return []; } else { let members = await MemberService.getAll({ noLimit: true, ids: queryMemberIds }); - return members[0]; + return members[0].filter((m) => m.sendNewsletter != null); } } @@ -154,14 +128,11 @@ export abstract class NewsletterHelper { let recipients = await NewsletterRecipientsService.getAll(newsletterId); let config = await NewsletterConfigService.getAll(); - let allowedForMail = config.filter((c) => c.config == NewsletterConfigType.mail).map((c) => c.comTypeId); + let allowedForMail = config.filter((c) => c.config == NewsletterConfigEnum.mail).map((c) => c.comTypeId); const members = await this.transformRecipientsToMembers(newsletter, recipients); const mailRecipients = members.filter( - (m) => - m.sendNewsletter != null && - m.sendNewsletter?.email != null && - allowedForMail.includes(m.sendNewsletter?.type?.id) + (m) => m.sendNewsletter?.email != "" && allowedForMail.includes(m.sendNewsletter?.type?.id) ); return mailRecipients; @@ -172,17 +143,17 @@ export abstract class NewsletterHelper { let recipients = await NewsletterRecipientsService.getAll(newsletterId); let config = await NewsletterConfigService.getAll(); - let notAllowedForPdf = config.filter((c) => c.config == NewsletterConfigType.mail).map((c) => c.comTypeId); + let notAllowedForPdf = config + .filter((c) => c.config == NewsletterConfigEnum.none || c.config == NewsletterConfigEnum.mail) + .map((c) => c.comTypeId); const members = await this.transformRecipientsToMembers(newsletter, recipients); - const pdfRecipients = members.filter( - (m) => !notAllowedForPdf.includes(m.sendNewsletter?.type?.id) || m.sendNewsletter == null - ); + const pdfRecipients = members.filter((m) => !notAllowedForPdf.includes(m.sendNewsletter?.type?.id)); pdfRecipients.unshift({ id: "0", firstname: "Alle Mitglieder", - lastname: CLUB_NAME, + lastname: SettingHelper.getSetting("club.name"), nameaffix: "", salutation: { salutation: "" }, } as member); @@ -224,11 +195,14 @@ export abstract class NewsletterHelper { const { body } = await TemplateHelper.renderFileForModule({ module: "newsletter", bodyData: data, - title: `Newsletter von ${CLUB_NAME}`, + title: `Newsletter von ${SettingHelper.getSetting("club.name")}`, }); - await MailHelper.sendMail(rec.sendNewsletter.email, `Newsletter von ${CLUB_NAME}`, body, [ - { filename: "events.ics", path: this.getICSFilePath(newsletter) }, - ]) + await MailHelper.sendMail( + rec.sendNewsletter.email, + `Newsletter von ${SettingHelper.getSetting("club.name")}`, + body, + [{ filename: "events.ics", path: this.getICSFilePath(newsletter) }] + ) .then(() => { this.formatJobEmit( "progress", @@ -278,7 +252,7 @@ export abstract class NewsletterHelper { if (error) throw new InternalException("Failed Building ICS form Pdf", error); this.saveIcsToFile(newsletter, value); - let printWithAdress = config.filter((c) => c.config == NewsletterConfigType.pdf).map((c) => c.comTypeId); + let printWithAdress = config.filter((c) => c.config == NewsletterConfigEnum.pdf).map((c) => c.comTypeId); const pdfRecipients = await this.getPrintRecipients(newsletterId); @@ -289,7 +263,7 @@ export abstract class NewsletterHelper { await PdfExport.renderFile({ template: "newsletter", - title: `Newsletter von ${CLUB_NAME}`, + title: `Newsletter von ${SettingHelper.getSetting("club.name")}`, filename: `${rec.lastname}_${rec.firstname}_${rec.id}`.replaceAll(" ", "-"), folder: `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`, data: data, diff --git a/src/helpers/permissionHelper.ts b/src/helpers/permissionHelper.ts index 6a83f92..d023f5c 100644 --- a/src/helpers/permissionHelper.ts +++ b/src/helpers/permissionHelper.ts @@ -17,33 +17,31 @@ export default class PermissionHelper { permissions: PermissionObject, type: PermissionType | "admin", section: PermissionSection, - module?: PermissionModule + module: PermissionModule ) { if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false; if (permissions?.admin || permissions?.adminByOwner) return true; if ( - (!module && - permissions[section] != undefined && - (permissions[section]?.all == "*" || permissions[section]?.all?.includes(type))) || permissions[section]?.all == "*" || - permissions[section]?.all?.includes(type) + permissions[section]?.all?.includes(type) || + permissions[section]?.[module] == "*" || + permissions[section]?.[module]?.includes(type) ) return true; - if (module && (permissions[section]?.[module] == "*" || permissions[section]?.[module]?.includes(type))) - return true; + return false; } static canSome( permissions: PermissionObject, checks: Array<{ - requiredPermissions: PermissionType | "admin"; + requiredPermission: PermissionType | "admin"; section: PermissionSection; - module?: PermissionModule; + module: PermissionModule; }> ) { - checks.reduce((prev, curr) => { - return prev || this.can(permissions, curr.requiredPermissions, curr.section, curr.module); + return checks.reduce((prev, curr) => { + return prev || this.can(permissions, curr.requiredPermission, curr.section, curr.module); }, false); } @@ -66,12 +64,29 @@ export default class PermissionHelper { static canSomeSection( permissions: PermissionObject, checks: Array<{ - requiredPermissions: PermissionType | "admin"; + requiredPermission: PermissionType | "admin"; section: PermissionSection; }> ): boolean { return checks.reduce((prev, curr) => { - return prev || this.can(permissions, curr.requiredPermissions, curr.section); + return prev || this.canSection(permissions, curr.requiredPermission, curr.section); + }, false); + } + + static canAccessSection(permissions: PermissionObject, section: PermissionSection): boolean { + if (permissions?.admin || permissions?.adminByOwner) return true; + if (permissions[section] != undefined) return true; + return false; + } + + static canAccessSomeSection( + permissions: PermissionObject, + checks: Array<{ + section: PermissionSection; + }> + ): boolean { + return checks.reduce((prev, curr) => { + return prev || this.canAccessSection(permissions, curr.section); }, false); } @@ -83,7 +98,7 @@ export default class PermissionHelper { static passCheckMiddleware( requiredPermissions: PermissionType | "admin", section: PermissionSection, - module?: PermissionModule + module: PermissionModule ): (req: Request, res: Response, next: Function) => void { return (req: Request, res: Response, next: Function) => { const permissions = req.permissions; @@ -99,9 +114,9 @@ export default class PermissionHelper { static passCheckSomeMiddleware( checks: Array<{ - requiredPermissions: PermissionType | "admin"; + requiredPermission: PermissionType | "admin"; section: PermissionSection; - module?: PermissionModule; + module: PermissionModule; }> ): (req: Request, res: Response, next: Function) => void { return (req: Request, res: Response, next: Function) => { @@ -111,9 +126,7 @@ export default class PermissionHelper { if (isOwner || this.canSome(permissions, checks)) { next(); } else { - let permissionsToPass = checks.reduce((prev, curr) => { - return prev + (prev != " or " ? "" : "") + `${curr.section}.${curr.module}.${curr.requiredPermissions}`; - }, ""); + let permissionsToPass = checks.map((c) => `${c.section}.${c.module}.${c.requiredPermission}`).join(" or "); throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`); } }; @@ -136,7 +149,7 @@ export default class PermissionHelper { } static sectionPassCheckSomeMiddleware( - checks: Array<{ requiredPermissions: PermissionType | "admin"; section: PermissionSection }> + checks: Array<{ requiredPermission: PermissionType | "admin"; section: PermissionSection }> ): (req: Request, res: Response, next: Function) => void { return (req: Request, res: Response, next: Function) => { const permissions = req.permissions; @@ -145,9 +158,38 @@ export default class PermissionHelper { if (isOwner || this.canSomeSection(permissions, checks)) { next(); } else { - let permissionsToPass = checks.reduce((prev, curr) => { - return prev + (prev != " or " ? "" : "") + `${curr.section}.${curr.requiredPermissions}`; - }, ""); + let permissionsToPass = checks.map((c) => `${c.section}.${c.requiredPermission}`).join(" or "); + throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`); + } + }; + } + + static sectionAccessPassCheckMiddleware( + section: PermissionSection + ): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + const permissions = req.permissions; + const isOwner = req.isOwner; + + if (isOwner || this.canAccessSection(permissions, section)) { + next(); + } else { + throw new ForbiddenRequestException(`missing permission for ${section}.${module}`); + } + }; + } + + static sectionAccessPassCheckSomeMiddleware( + checks: Array<{ section: PermissionSection }> + ): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + const permissions = req.permissions; + const isOwner = req.isOwner; + + if (isOwner || this.canAccessSomeSection(permissions, checks)) { + next(); + } else { + let permissionsToPass = checks.map((c) => `${c.section}`).join(" or "); throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`); } }; diff --git a/src/helpers/settingsHelper.ts b/src/helpers/settingsHelper.ts new file mode 100644 index 0000000..d4c21d8 --- /dev/null +++ b/src/helpers/settingsHelper.ts @@ -0,0 +1,286 @@ +import { SettingString, settingsType, SettingTopic, SettingTypeAtom, SettingValueMapping } from "../type/settingTypes"; +import { CodingHelper } from "./codingHelper"; +import SettingCommandHandler from "../command/management/setting/settingCommandHandler"; +import SettingService from "../service/management/settingService"; +import { APPLICATION_SECRET } from "../env.defaults"; +import { + BooleanConverter, + EmailConverter, + LongStringConverter, + MsConverter, + NumberConverter, + StringConverter, + TypeConverter, + UrlConverter, +} from "./convertHelper"; +import cloneDeep from "lodash.clonedeep"; +import { rejects } from "assert"; +import InternalException from "../exceptions/internalException"; +import MailHelper from "./mailHelper"; + +export default abstract class SettingHelper { + private static settings: { [key in SettingString]?: string } = {}; + + private static listeners: Map void>> = new Map(); + private static topicListeners: Map void>> = new Map(); + + private static readonly converters: Record> = { + longstring: new LongStringConverter(), + string: new StringConverter(), + url: new UrlConverter(), + number: new NumberConverter(), + boolean: new BooleanConverter(), + ms: new MsConverter(), + email: new EmailConverter(), + }; + + public static getAllSettings(): { [key in SettingString]: SettingValueMapping[key] } { + return Object.keys(settingsType).reduce((acc, key) => { + const typedKey = key as SettingString; + //@ts-expect-error + acc[typedKey] = this.getSetting(typedKey); + return acc; + }, {} as { [key in SettingString]: SettingValueMapping[key] }); + } + + /** + * Returns the value of a setting with the correct type based on the key + * @param key The key of the setting + * @returns The typed value of the setting + */ + public static getSetting(key: K): SettingValueMapping[K] { + const settingType = settingsType[key]; + const rawValue = this.settings[key] ?? String(settingType.default ?? ""); + + if (Array.isArray(settingType.type)) { + return rawValue as unknown as SettingValueMapping[K]; + } + + let processedValue = rawValue; + if (typeof settingType.type === "string" && settingType.type.includes("/crypt") && processedValue != "") { + processedValue = CodingHelper.decrypt(APPLICATION_SECRET, processedValue); + } + + const baseType = + typeof settingType.type === "string" + ? (settingType.type.split("/")[0] as SettingTypeAtom) + : (settingType.type as SettingTypeAtom); + + return this.converters[baseType].fromString(processedValue) as unknown as SettingValueMapping[K]; + } + + /** + * Sets a setting + * undefined value leads to reset of key + * @param key The key of the setting + * @param value The value to set + */ + public static async setSetting(key: K, value: SettingValueMapping[K]): Promise { + if (value === undefined || value === null || value === "") { + if (key != "mail.password") this.resetSetting(key); + return; + } + + const stringValue = String(value); + + const settingType = settingsType[key]; + this.validateSetting(key, stringValue); + + const oldValue = cloneDeep(this.settings[key]); + let newValue = stringValue; + + if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) { + newValue = CodingHelper.encrypt(APPLICATION_SECRET, stringValue); + } + + this.settings[key] = newValue; + const [topic, settingKey] = key.split(".") as [SettingTopic, string]; + + await SettingCommandHandler.create({ + topic, + key: settingKey, + value: newValue, + }); + + this.notifyListeners(key, newValue, oldValue); + } + + /** + * Resets a setting to its default value + * @param key The key of the setting + */ + public static async resetSetting(key: SettingString): Promise { + if (this.getSetting(key) == String(settingsType[key].default ?? "")) return; + + const oldValue = this.getSetting(key); + + const settingType = settingsType[key]; + this.settings[key] = String(settingType.default ?? ""); + + const [topic, settingKey] = key.split(".") as [SettingTopic, string]; + await SettingCommandHandler.delete({ + topic, + key: settingKey, + }); + + const newValue = this.getSetting(key); + this.notifyListeners(key, newValue, oldValue); + } + + public static async configure(): Promise { + console.log("Configuring Settings"); + const settings = await SettingService.getSettings(); + + for (const element of settings) { + const ref = `${element.topic}.${element.key}` as SettingString; + this.settings[ref] = element.value; + + try { + this.validateSetting(ref); + } catch (error) { + console.warn(`Invalid setting ${ref}: ${error.message}`); + } + } + } + + public static async checkMail( + setting: Array<{ key: K; value: SettingValueMapping[K] }> + ): Promise { + return new Promise(async (resolve, reject) => { + if (setting.some((t) => t.key == "mail.email" && t.value != undefined)) { + let emailValue = setting.find((t) => t.key == "mail.email").value as string; + let checkMail = await MailHelper.checkMail(emailValue); + if (!checkMail) { + return reject("mail"); + } + } + + if (setting.some((t) => t.key.startsWith("mail"))) { + let checkConfig = await MailHelper.verifyTransport({ + user: + (setting.find((t) => t.key == "mail.username").value as string) ?? + SettingHelper.getSetting("mail.username"), + password: + (setting.find((t) => t.key == "mail.password").value as string) ?? + SettingHelper.getSetting("mail.password"), + host: (setting.find((t) => t.key == "mail.host").value as string) ?? SettingHelper.getSetting("mail.host"), + port: (setting.find((t) => t.key == "mail.port").value as number) ?? SettingHelper.getSetting("mail.port"), + secure: + (setting.find((t) => t.key == "mail.secure").value as boolean) ?? SettingHelper.getSetting("mail.secure"), + }); + if (!checkConfig) { + return reject("Config is not valid"); + } + } + resolve(); + }); + } + + /** + * Validates a setting + * @param key The key of the setting + * @param value Optional value to validate + */ + private static validateSetting(key: SettingString, value?: string): void { + const settingType = settingsType[key]; + const valueToCheck = value ?? this.settings[key] ?? String(settingType.default ?? ""); + + if (Array.isArray(settingType.type)) { + return; + } + + const baseType = + typeof settingType.type === "string" + ? (settingType.type.split("/")[0] as SettingTypeAtom) + : (settingType.type as SettingTypeAtom); + + if (!this.converters[baseType].validate(valueToCheck)) { + throw new Error(`Invalid value for ${key} of type ${baseType}`); + } + + if (baseType === "number" && settingType.min !== undefined) { + const numValue = Number(valueToCheck); + if (numValue < settingType.min) { + throw new Error(`${key} must be at least ${settingType.min}`); + } + } + } + + /** + * Registers a listener for changes to a specific setting + * @param key The setting to monitor + * @param callback Function to be called when changes occur + */ + public static onSettingChanged( + key: K, + callback: (newValue: SettingValueMapping[K], oldValue: SettingValueMapping[K]) => void + ): void { + if (!this.listeners.has(key)) { + this.listeners.set(key, []); + } + + this.listeners.get(key)!.push(callback); + } + + /** + * Registers a listener for changes to a specific setting + * @param key The setting to monitor + * @param callback Function to be called when changes occur + */ + public static onSettingTopicChanged(key: K, callback: () => void): void { + if (!this.topicListeners.has(key)) { + this.topicListeners.set(key, []); + } + + this.topicListeners.get(key)!.push(callback); + } + + /** + * Removes a registered listener + * @param key The setting + * @param callback The callback to remove + */ + public static removeSettingListener( + key: K, + callback: (newValue: SettingValueMapping[K], oldValue: SettingValueMapping[K]) => void + ): void { + if (!this.listeners.has(key)) return; + + const callbacks = this.listeners.get(key)!; + const index = callbacks.indexOf(callback); + + if (index !== -1) { + callbacks.splice(index, 1); + } + + if (callbacks.length === 0) { + this.listeners.delete(key); + } + } + + /** + * Notifies all registered listeners about changes + * @param key The changed setting + * @param newValue The new value + * @param oldValue The old value + */ + private static notifyListeners(key: SettingString, newValue: any, oldValue: any): void { + const callbacks = this.listeners.get(key) ?? []; + for (const callback of callbacks) { + try { + callback(newValue, oldValue); + } catch (error) { + console.error(`Error in setting listener for ${key}:`, error); + } + } + + const topicCallbacks = this.topicListeners.get(key.split(".")[0] as SettingTopic) ?? []; + for (const callback of topicCallbacks) { + try { + callback(); + } catch (error) { + console.error(`Error in setting listener for ${key.split(".")[0]}:`, error); + } + } + } +} diff --git a/src/helpers/templateHelper.ts b/src/helpers/templateHelper.ts index 3dc5ad8..dd77a74 100644 --- a/src/helpers/templateHelper.ts +++ b/src/helpers/templateHelper.ts @@ -9,10 +9,10 @@ export abstract class TemplateHelper { static getTemplateFromFile(template: string) { let tmpFile; try { - tmpFile = FileSystemHelper.readTemplateFile(`/src/templates/${template}.template.html`); + tmpFile = FileSystemHelper.readTemplateFile(`${template}.template.html`); } catch (err) { tmpFile = FileSystemHelper.readTemplateFile( - `/src/templates/${template.split(".")[template.split(".").length - 1]}.template.html` + `${template.split(".")[template.split(".").length - 1]}.template.html` ); } return tmpFile; diff --git a/src/index.ts b/src/index.ts index a282863..13de2ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import "dotenv/config"; import "./handlebars.config"; import express from "express"; -import { BACKUP_AUTO_RESTORE, configCheck, SERVER_PORT } from "./env.defaults"; +import { configCheck } from "./env.defaults"; configCheck(); import { PermissionObject } from "./type/permissionTypes"; @@ -21,23 +21,29 @@ declare global { import { dataSource } from "./data-source"; import BackupHelper from "./helpers/backupHelper"; +import SettingHelper from "./helpers/settingsHelper"; dataSource.initialize().then(async () => { - if ((BACKUP_AUTO_RESTORE as "true" | "false") == "true" && (await dataSource.createQueryRunner().hasTable("user"))) { + if (await dataSource.createQueryRunner().hasTable("user")) { await BackupHelper.autoRestoreBackup().catch((err) => { console.log(`${new Date().toISOString()}: failed auto-restoring database`, err); }); } + await SettingHelper.configure(); + MailHelper.initialize(); }); const app = express(); import router from "./routes/index"; router(app); -app.listen(process.env.NODE_ENV ? SERVER_PORT : 5000, () => { - console.log(`${new Date().toISOString()}: listening on port ${process.env.NODE_ENV ? SERVER_PORT : 5000}`); +app.listen(process.env.NODE_ENV ? process.env.SERVER_PORT ?? 5000 : 5000, () => { + console.log( + `${new Date().toISOString()}: listening on port ${process.env.NODE_ENV ? process.env.SERVER_PORT ?? 5000 : 5000}` + ); }); import schedule from "node-schedule"; import RefreshCommandHandler from "./command/refreshCommandHandler"; +import MailHelper from "./helpers/mailHelper"; const job = schedule.scheduleJob("0 0 * * *", async () => { console.log(`${new Date().toISOString()}: running Cron`); await RefreshCommandHandler.deleteExpired(); diff --git a/src/middleware/multer.ts b/src/middleware/multer.ts new file mode 100644 index 0000000..ebcec4e --- /dev/null +++ b/src/middleware/multer.ts @@ -0,0 +1,35 @@ +import multer from "multer"; +import { FileSystemHelper } from "../helpers/fileSystemHelper"; +import path from "path"; +import BadRequestException from "../exceptions/badRequestException"; + +export const clubImageStorage = multer.diskStorage({ + destination: FileSystemHelper.formatPath("/app"), + filename: function (req, file, cb) { + const fileExtension = path.extname(file.originalname).toLowerCase(); + + if (file.fieldname === "icon") { + cb(null, "admin-icon" + fileExtension); + } else if (file.fieldname === "logo") { + cb(null, "admin-logo" + fileExtension); + } else { + cb(null, file.originalname); + } + }, +}); + +export const clubImageMulter = multer({ + storage: clubImageStorage, + fileFilter(req, file, cb) { + if (file.mimetype.startsWith("image/png")) { + cb(null, true); + } else { + cb(new BadRequestException("Wrong file format")); + } + }, +}); + +export const clubImageUpload = clubImageMulter.fields([ + { name: "icon", maxCount: 1 }, + { name: "logo", maxCount: 1 }, +]); diff --git a/src/migrations/1738166167472-CreateSchema.ts b/src/migrations/1738166167472-CreateSchema.ts deleted file mode 100644 index df9c6fd..0000000 --- a/src/migrations/1738166167472-CreateSchema.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; -import { - invite_table, - refresh_table, - reset_table, - role_permission_table, - role_table, - user_permission_table, - user_roles_table, - user_table, - webapi_permission_table, - webapi_table, -} from "./baseSchemaTables/admin"; -import { templateUsage } from "../entity/configuration/templateUsage"; -import { - award_table, - communication_type_table, - executive_position_table, - member_awards_table, - member_communication_table, - member_executive_positions_table, - member_executive_positions_view_mysql, - member_executive_positions_view_postgres, - member_executive_positions_view_sqlite, - member_qualifications_table, - member_qualifications_view_mysql, - member_qualifications_view_postgres, - member_qualifications_view_sqlite, - member_table, - member_view_mysql, - member_view_postgres, - member_view_sqlite, - membership_status_table, - membership_table, - membership_view_mysql, - membership_view_postgres, - membership_view_sqlite, - qualification_table, - salutation_table, -} from "./baseSchemaTables/member"; -import { query_table, template_table, template_usage_table } from "./baseSchemaTables/query_template"; -import { - protocol_agenda_table, - protocol_decision_table, - protocol_presence_table, - protocol_printout_table, - protocol_table, - protocol_voting_table, -} from "./baseSchemaTables/protocol"; -import { calendar_table, calendar_type_table } from "./baseSchemaTables/calendar"; -import { - newsletter_config_table, - newsletter_dates_table, - newsletter_recipients_table, - newsletter_table, -} from "./baseSchemaTables/newsletter"; -import { DB_TYPE } from "../env.defaults"; - -export class CreateSchema1738166167472 implements MigrationInterface { - name = "CreateSchema1738166167472"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable(reset_table, true, true, true); - await queryRunner.createTable(invite_table, true, true, true); - await queryRunner.createTable(role_table, true, true, true); - await queryRunner.createTable(role_permission_table, true, true, true); - await queryRunner.createTable(user_table, true, true, true); - await queryRunner.createTable(user_roles_table, true, true, true); - await queryRunner.createTable(user_permission_table, true, true, true); - await queryRunner.createTable(refresh_table, true, true, true); - await queryRunner.createTable(webapi_table, true, true, true); - await queryRunner.createTable(webapi_permission_table, true, true, true); - - await queryRunner.createTable(salutation_table, true, true, true); - await queryRunner.createTable(award_table, true, true, true); - await queryRunner.createTable(communication_type_table, true, true, true); - await queryRunner.createTable(membership_status_table, true, true, true); - await queryRunner.createTable(executive_position_table, true, true, true); - await queryRunner.createTable(qualification_table, true, true, true); - await queryRunner.createTable(member_table, true, true, true); - await queryRunner.createTable(member_awards_table, true, true, true); - await queryRunner.createTable(member_communication_table, true, true, true); - await queryRunner.createTable(membership_table, true, true, true); - await queryRunner.createTable(member_executive_positions_table, true, true, true); - await queryRunner.createTable(member_qualifications_table, true, true, true); - - if (DB_TYPE == "postgres") await queryRunner.createView(member_view_postgres, true); - else if (DB_TYPE == "mysql") await queryRunner.createView(member_view_mysql, true); - else if (DB_TYPE == "sqlite") await queryRunner.createView(member_view_sqlite, true); - if (DB_TYPE == "postgres") await queryRunner.createView(membership_view_postgres, true); - else if (DB_TYPE == "mysql") await queryRunner.createView(membership_view_mysql, true); - else if (DB_TYPE == "sqlite") await queryRunner.createView(membership_view_sqlite, true); - if (DB_TYPE == "postgres") await queryRunner.createView(member_qualifications_view_postgres, true); - else if (DB_TYPE == "mysql") await queryRunner.createView(member_qualifications_view_mysql, true); - else if (DB_TYPE == "sqlite") await queryRunner.createView(member_qualifications_view_sqlite, true); - if (DB_TYPE == "postgres") await queryRunner.createView(member_executive_positions_view_postgres, true); - else if (DB_TYPE == "mysql") await queryRunner.createView(member_executive_positions_view_mysql, true); - else if (DB_TYPE == "sqlite") await queryRunner.createView(member_executive_positions_view_sqlite, true); - - await queryRunner.createTable(query_table, true, true, true); - await queryRunner.createTable(template_table, true, true, true); - await queryRunner.createTable(template_usage_table, true, true, true); - - await queryRunner.manager - .createQueryBuilder() - .insert() - .into(templateUsage) - .values([{ scope: "newsletter" }, { scope: "protocol" }, { scope: "member.list" }]) - .orIgnore() - .execute(); - - await queryRunner.createTable(protocol_table, true, true, true); - await queryRunner.createTable(protocol_agenda_table, true, true, true); - await queryRunner.createTable(protocol_decision_table, true, true, true); - await queryRunner.createTable(protocol_presence_table, true, true, true); - await queryRunner.createTable(protocol_voting_table, true, true, true); - await queryRunner.createTable(protocol_printout_table, true, true, true); - - await queryRunner.createTable(calendar_type_table, true, true, true); - await queryRunner.createTable(calendar_table, true, true, true); - - await queryRunner.createTable(newsletter_config_table, true, true, true); - await queryRunner.createTable(newsletter_table, true, true, true); - await queryRunner.createTable(newsletter_dates_table, true, true, true); - await queryRunner.createTable(newsletter_recipients_table, true, true, true); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable("newsletter_dates", true, true, true); - await queryRunner.dropTable("newsletter_recipients", true, true, true); - await queryRunner.dropTable("newsletter", true, true, true); - await queryRunner.dropTable("newsletter_config", true, true, true); - - await queryRunner.dropTable("calendar", true, true, true); - await queryRunner.dropTable("calendar_type", true, true, true); - - await queryRunner.dropTable("protocol_agenda", true, true, true); - await queryRunner.dropTable("protocol_decision", true, true, true); - await queryRunner.dropTable("protocol_presence", true, true, true); - await queryRunner.dropTable("protocol_voting", true, true, true); - await queryRunner.dropTable("protocol_printout", true, true, true); - await queryRunner.dropTable("protocol", true, true, true); - - await queryRunner.dropTable("template_usage", true, true, true); - await queryRunner.dropTable("template", true, true, true); - await queryRunner.dropTable("query", true, true, true); - - await queryRunner.dropView("member_view"); - await queryRunner.dropView("membership_view"); - await queryRunner.dropView("member_qualifications_view"); - await queryRunner.dropView("member_executive_positions_view"); - - await queryRunner.dropTable("member_awards", true, true, true); - await queryRunner.dropTable("communication", true, true, true); - await queryRunner.dropTable("membership", true, true, true); - await queryRunner.dropTable("member_executive_positions", true, true, true); - await queryRunner.dropTable("member_qualifications", true, true, true); - await queryRunner.dropTable("member", true, true, true); - await queryRunner.dropTable("salutation", true, true, true); - await queryRunner.dropTable("award", true, true, true); - await queryRunner.dropTable("communication_type", true, true, true); - await queryRunner.dropTable("membership_status", true, true, true); - await queryRunner.dropTable("executive_position", true, true, true); - await queryRunner.dropTable("qualification", true, true, true); - - await queryRunner.dropTable("webapi_permission", true, true, true); - await queryRunner.dropTable("webapi", true, true, true); - await queryRunner.dropTable("refresh", true, true, true); - await queryRunner.dropTable("user_permission", true, true, true); - await queryRunner.dropTable("user_roles", true, true, true); - await queryRunner.dropTable("user", true, true, true); - await queryRunner.dropTable("role_permission", true, true, true); - await queryRunner.dropTable("role", true, true, true); - await queryRunner.dropTable("invite", true, true, true); - await queryRunner.dropTable("reset", true, true, true); - } -} diff --git a/src/migrations/1742549956787-templatesAndProtocolSort.ts b/src/migrations/1742549956787-templatesAndProtocolSort.ts deleted file mode 100644 index cc86bf8..0000000 --- a/src/migrations/1742549956787-templatesAndProtocolSort.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; -import { templateUsage } from "../entity/configuration/templateUsage"; -import { getTypeByORM, getDefaultByORM } from "./ormHelper"; - -export class TemplatesAndProtocolSort1742549956787 implements MigrationInterface { - name = "TemplatesAndProtocolSort1742549956787"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.manager - .createQueryBuilder() - .insert() - .into(templateUsage) - .values([{ scope: "member" }]) - .orIgnore() - .execute(); - - await queryRunner.manager - .createQueryBuilder() - .delete() - .from(templateUsage) - .where({ scope: "member.list" }) - .execute(); - - await queryRunner.addColumn( - "protocol_agenda", - new TableColumn({ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }) - ); - - await queryRunner.addColumn( - "protocol_decision", - new TableColumn({ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }) - ); - - await queryRunner.addColumn( - "protocol_voting", - new TableColumn({ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }) - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropColumn("protocol_agenda", "sort"); - await queryRunner.dropColumn("protocol_decision", "sort"); - await queryRunner.dropColumn("protocol_voting", "sort"); - - await queryRunner.manager - .createQueryBuilder() - .insert() - .into(templateUsage) - .values([{ scope: "member.list" }]) - .orIgnore() - .execute(); - - await queryRunner.manager.createQueryBuilder().delete().from(templateUsage).where({ scope: "member" }).execute(); - } -} diff --git a/src/migrations/1742922178643-queryToUUID.ts b/src/migrations/1742922178643-queryToUUID.ts deleted file mode 100644 index b52990e..0000000 --- a/src/migrations/1742922178643-queryToUUID.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey } from "typeorm"; -import { getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "./ormHelper"; -import { query } from "../entity/configuration/query"; - -export class QueryToUUID1742922178643 implements MigrationInterface { - name = "QueryToUUID1742922178643"; - - public async up(queryRunner: QueryRunner): Promise { - const table = await queryRunner.getTable("newsletter"); - const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("recipientsByQueryId") !== -1); - await queryRunner.dropForeignKey("newsletter", foreignKey); - - const entries = await queryRunner.manager.getRepository(query).find({ select: { title: true, query: true } }); - await queryRunner.clearTable("query"); - - await queryRunner.dropColumn("newsletter", "recipientsByQueryId"); - await queryRunner.dropColumn("query", "id"); - - await queryRunner.addColumn( - "query", - new TableColumn({ - name: "id", - ...getTypeByORM("uuid"), - ...isUUIDPrimary, - }) - ); - await queryRunner.addColumn( - "newsletter", - new TableColumn({ - name: "recipientsByQueryId", - ...getTypeByORM("uuid", true), - }) - ); - - await queryRunner.manager.createQueryBuilder().insert().into("query").values(entries).execute(); - - await queryRunner.createForeignKey( - "newsletter", - new TableForeignKey({ - columnNames: ["recipientsByQueryId"], - referencedColumnNames: ["id"], - referencedTableName: "query", - onDelete: "CASCADE", - onUpdate: "RESTRICT", - }) - ); - } - - public async down(queryRunner: QueryRunner): Promise { - const table = await queryRunner.getTable("newsletter"); - const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("recipientsByQueryId") !== -1); - await queryRunner.dropForeignKey("newsletter", foreignKey); - - const entries = await queryRunner.manager.getRepository(query).find({ select: { title: true, query: true } }); - await queryRunner.clearTable("query"); - - await queryRunner.dropColumn("newsletter", "recipientsByQueryId"); - await queryRunner.dropColumn("query", "id"); - - await queryRunner.addColumn( - "query", - new TableColumn({ - name: "id", - ...getTypeByORM("int"), - ...isIncrementPrimary, - }) - ); - await queryRunner.addColumn( - "newsletter", - new TableColumn({ - name: "recipientsByQueryId", - ...getTypeByORM("int", true), - }) - ); - - await queryRunner.manager - .createQueryBuilder() - .insert() - .into("query") - .values(entries.map((e, i) => ({ ...e, id: i + 1 }))) - .execute(); - - await queryRunner.createForeignKey( - "newsletter", - new TableForeignKey({ - columnNames: ["recipientsByQueryId"], - referencedColumnNames: ["id"], - referencedTableName: "query", - onDelete: "CASCADE", - onUpdate: "RESTRICT", - }) - ); - } -} diff --git a/src/migrations/1744351418751-newsletterColumnType.ts b/src/migrations/1744351418751-newsletterColumnType.ts deleted file mode 100644 index 6efacfa..0000000 --- a/src/migrations/1744351418751-newsletterColumnType.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; -import { getDefaultByORM, getTypeByORM } from "./ormHelper"; -import { newsletter } from "../entity/club/newsletter/newsletter"; - -export class NewsletterColumnType1744351418751 implements MigrationInterface { - name = "NewsletterColumnType1744351418751"; - - public async up(queryRunner: QueryRunner): Promise { - let newsletters = await queryRunner.manager.getRepository("newsletter").find(); - - await queryRunner.dropColumn("newsletter", "newsletterTitle"); - await queryRunner.dropColumn("newsletter", "newsletterSignatur"); - - await queryRunner.addColumn( - "newsletter", - new TableColumn({ name: "newsletterTitle", ...getTypeByORM("text"), default: getDefaultByORM("string") }) - ); - await queryRunner.addColumn( - "newsletter", - new TableColumn({ name: "newsletterSignatur", ...getTypeByORM("text"), default: getDefaultByORM("string") }) - ); - - await queryRunner.manager.getRepository("newsletter").save(newsletters); - } - - public async down(queryRunner: QueryRunner): Promise { - let newsletters = await queryRunner.manager.getRepository("newsletter").find(); - - await queryRunner.dropColumn("newsletter", "newsletterTitle"); - await queryRunner.dropColumn("newsletter", "newsletterSignatur"); - - await queryRunner.addColumn( - "newsletter", - new TableColumn({ name: "newsletterTitle", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }) - ); - await queryRunner.addColumn( - "newsletter", - new TableColumn({ name: "newsletterSignatur", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }) - ); - - await queryRunner.manager.getRepository("newsletter").save(newsletters); - } -} diff --git a/src/migrations/1744795756230-QueryUpdatedAt.ts b/src/migrations/1744795756230-QueryUpdatedAt.ts deleted file mode 100644 index 428ba0c..0000000 --- a/src/migrations/1744795756230-QueryUpdatedAt.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; -import { getTypeByORM, getDefaultByORM } from "./ormHelper"; - -export class QueryUpdatedAt1744795756230 implements MigrationInterface { - name = "QueryUpdatedAt1744795756230"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.addColumn( - "query", - new TableColumn({ - name: "updatedAt", - ...getTypeByORM("datetime", false, 6), - default: getDefaultByORM("currentTimestamp", 6), - onUpdate: getDefaultByORM("currentTimestamp", 6), - }) - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropColumn("query", "updatedAt"); - } -} diff --git a/src/migrations/1738166124200-BackupAndResetDatabase.ts b/src/migrations/1749296262915-BackupAndResetDatabase.ts similarity index 73% rename from src/migrations/1738166124200-BackupAndResetDatabase.ts rename to src/migrations/1749296262915-BackupAndResetDatabase.ts index f0656ba..fa31663 100644 --- a/src/migrations/1738166124200-BackupAndResetDatabase.ts +++ b/src/migrations/1749296262915-BackupAndResetDatabase.ts @@ -1,26 +1,24 @@ import { MigrationInterface, QueryRunner, Table } from "typeorm"; -import BackupHelper from "../helpers/backupHelper"; -import { getDefaultByORM, getTypeByORM, isIncrementPrimary } from "./ormHelper"; import InternalException from "../exceptions/internalException"; -import { DB_TYPE } from "../env.defaults"; +import BackupHelper from "../helpers/backupHelper"; +import { getTypeByORM, isIncrementPrimary, getDefaultByORM } from "./ormHelper"; -export class BackupAndResetDatabase1738166124200 implements MigrationInterface { - name = "BackupAndResetDatabase1738166124200"; +export class BackupAndResetDatabase1749296262915 implements MigrationInterface { + name = "BackupAndResetDatabase1749296262915"; public async up(queryRunner: QueryRunner): Promise { - let query = DB_TYPE == "postgres" ? "SELECT name FROM migrations" : "SELECT `name` FROM `migrations`"; - let migrations = await queryRunner.query(query); + let migrations = await queryRunner.query("SELECT name FROM migrations"); if ( (await queryRunner.hasTable("user")) && - migrations.findIndex((m: any) => m.name == "MoveSendNewsletterFlag1737816852011") == -1 + migrations.findIndex((m: any) => m.name == "MemberExtendData1748953828644") == -1 ) { throw new InternalException( - "Cannot update due to skiped version. Update to v1.2.2 Version first to prevent data loss and get access to the newer Versions." + "Cannot update due to skiped version. Update to v1.6.0 Version first to prevent data loss and get access to the newer Versions." ); } if (await queryRunner.hasTable("user")) { - await BackupHelper.createBackup({ collectIds: false }); + await BackupHelper.createBackup({ collectIds: true }); } await queryRunner.clearDatabase(); diff --git a/src/migrations/1749296280721-CreateSchema.ts b/src/migrations/1749296280721-CreateSchema.ts new file mode 100644 index 0000000..638455c --- /dev/null +++ b/src/migrations/1749296280721-CreateSchema.ts @@ -0,0 +1,176 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { + reset_table, + invite_table, + role_table, + role_permission_table, + user_table, + user_roles_table, + user_permission_table, + refresh_table, + webapi_table, + webapi_permission_table, + setting_table, +} from "./baseSchemaTables/admin"; +import { calendar_type_table, calendar_table } from "./baseSchemaTables/calendar"; +import { + salutation_table, + award_table, + communication_type_table, + membership_status_table, + executive_position_table, + qualification_table, + member_table, + member_awards_table, + member_communication_table, + membership_table, + member_executive_positions_table, + member_qualifications_table, + member_view, + membership_view, + member_qualifications_view, + member_executive_positions_view, + education_table, + member_educations_table, + membership_total_view, +} from "./baseSchemaTables/member"; +import { + newsletter_config_table, + newsletter_table, + newsletter_dates_table, + newsletter_recipients_table, +} from "./baseSchemaTables/newsletter"; +import { + protocol_table, + protocol_agenda_table, + protocol_decision_table, + protocol_presence_table, + protocol_voting_table, + protocol_printout_table, +} from "./baseSchemaTables/protocol"; +import { query_table, template_table, template_usage_table } from "./baseSchemaTables/query_template"; +import { availableTemplates } from "../type/templateTypes"; + +export class CreateSchema1749296280721 implements MigrationInterface { + name = "CreateSchema1749296280721"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable(reset_table, true, true, true); + await queryRunner.createTable(invite_table, true, true, true); + await queryRunner.createTable(role_table, true, true, true); + await queryRunner.createTable(role_permission_table, true, true, true); + await queryRunner.createTable(user_table, true, true, true); + await queryRunner.createTable(user_roles_table, true, true, true); + await queryRunner.createTable(user_permission_table, true, true, true); + await queryRunner.createTable(refresh_table, true, true, true); + await queryRunner.createTable(webapi_table, true, true, true); + await queryRunner.createTable(webapi_permission_table, true, true, true); + await queryRunner.createTable(setting_table, true, true, true); + + await queryRunner.createTable(salutation_table, true, true, true); + await queryRunner.createTable(award_table, true, true, true); + await queryRunner.createTable(communication_type_table, true, true, true); + await queryRunner.createTable(membership_status_table, true, true, true); + await queryRunner.createTable(executive_position_table, true, true, true); + await queryRunner.createTable(qualification_table, true, true, true); + await queryRunner.createTable(education_table, true, true, true); + await queryRunner.createTable(member_table, true, true, true); + await queryRunner.createTable(member_awards_table, true, true, true); + await queryRunner.createTable(member_communication_table, true, true, true); + await queryRunner.createTable(membership_table, true, true, true); + await queryRunner.createTable(member_executive_positions_table, true, true, true); + await queryRunner.createTable(member_qualifications_table, true, true, true); + await queryRunner.createTable(member_educations_table, true, true, true); + + await queryRunner.createView(member_view, true); + await queryRunner.createView(membership_view, true); + await queryRunner.createView(membership_total_view, true); + await queryRunner.createView(member_qualifications_view, true); + await queryRunner.createView(member_executive_positions_view, true); + + await queryRunner.createTable(query_table, true, true, true); + await queryRunner.createTable(template_table, true, true, true); + await queryRunner.createTable(template_usage_table, true, true, true); + + await queryRunner.manager + .createQueryBuilder() + .insert() + .into(template_usage_table.name) + .values( + availableTemplates.map((at) => ({ + scope: at, + })) + ) + .orIgnore() + .execute(); + + await queryRunner.createTable(protocol_table, true, true, true); + await queryRunner.createTable(protocol_agenda_table, true, true, true); + await queryRunner.createTable(protocol_decision_table, true, true, true); + await queryRunner.createTable(protocol_presence_table, true, true, true); + await queryRunner.createTable(protocol_voting_table, true, true, true); + await queryRunner.createTable(protocol_printout_table, true, true, true); + + await queryRunner.createTable(calendar_type_table, true, true, true); + await queryRunner.createTable(calendar_table, true, true, true); + + await queryRunner.createTable(newsletter_config_table, true, true, true); + await queryRunner.createTable(newsletter_table, true, true, true); + await queryRunner.createTable(newsletter_dates_table, true, true, true); + await queryRunner.createTable(newsletter_recipients_table, true, true, true); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable(newsletter_dates_table, true, true, true); + await queryRunner.dropTable(newsletter_recipients_table, true, true, true); + await queryRunner.dropTable(newsletter_table, true, true, true); + await queryRunner.dropTable(newsletter_config_table, true, true, true); + + await queryRunner.dropTable(calendar_table, true, true, true); + await queryRunner.dropTable(calendar_type_table, true, true, true); + + await queryRunner.dropTable(protocol_agenda_table, true, true, true); + await queryRunner.dropTable(protocol_decision_table, true, true, true); + await queryRunner.dropTable(protocol_presence_table, true, true, true); + await queryRunner.dropTable(protocol_voting_table, true, true, true); + await queryRunner.dropTable(protocol_printout_table, true, true, true); + await queryRunner.dropTable(protocol_table, true, true, true); + + await queryRunner.dropTable(template_usage_table, true, true, true); + await queryRunner.dropTable(template_table, true, true, true); + await queryRunner.dropTable(query_table, true, true, true); + + await queryRunner.dropView(member_view); + await queryRunner.dropView(membership_view); + await queryRunner.dropView(membership_total_view); + await queryRunner.dropView(member_qualifications_view); + await queryRunner.dropView(member_executive_positions_view); + + await queryRunner.dropTable(member_awards_table, true, true, true); + await queryRunner.dropTable(member_communication_table, true, true, true); + await queryRunner.dropTable(membership_table, true, true, true); + await queryRunner.dropTable(member_executive_positions_table, true, true, true); + await queryRunner.dropTable(member_qualifications_table, true, true, true); + await queryRunner.dropTable(member_educations_table, true, true, true); + await queryRunner.dropTable(member_table, true, true, true); + await queryRunner.dropTable(salutation_table, true, true, true); + await queryRunner.dropTable(award_table, true, true, true); + await queryRunner.dropTable(communication_type_table, true, true, true); + await queryRunner.dropTable(membership_status_table, true, true, true); + await queryRunner.dropTable(executive_position_table, true, true, true); + await queryRunner.dropTable(qualification_table, true, true, true); + await queryRunner.dropTable(education_table, true, true, true); + + await queryRunner.dropTable(setting_table, true, true, true); + await queryRunner.dropTable(webapi_permission_table, true, true, true); + await queryRunner.dropTable(webapi_table, true, true, true); + await queryRunner.dropTable(refresh_table, true, true, true); + await queryRunner.dropTable(user_permission_table, true, true, true); + await queryRunner.dropTable(user_roles_table, true, true, true); + await queryRunner.dropTable(user_table, true, true, true); + await queryRunner.dropTable(role_permission_table, true, true, true); + await queryRunner.dropTable(role_table, true, true, true); + await queryRunner.dropTable(invite_table, true, true, true); + await queryRunner.dropTable(reset_table, true, true, true); + } +} diff --git a/src/migrations/baseSchemaTables/admin.ts b/src/migrations/baseSchemaTables/admin.ts index c3eb94b..b898aba 100644 --- a/src/migrations/baseSchemaTables/admin.ts +++ b/src/migrations/baseSchemaTables/admin.ts @@ -1,5 +1,6 @@ -import { Table, TableForeignKey } from "typeorm"; +import { Table, TableForeignKey, TableIndex, TableUnique } from "typeorm"; import { getDefaultByORM, getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "../ormHelper"; +import { LoginRoutineEnum } from "../../enums/loginRoutineEnum"; export const invite_table = new Table({ name: "invite", @@ -16,7 +17,12 @@ export const role_table = new Table({ name: "role", columns: [ { name: "id", ...getTypeByORM("int"), ...isIncrementPrimary }, - { name: "role", ...getTypeByORM("varchar"), isUnique: true }, + { name: "role", ...getTypeByORM("varchar") }, + ], + uniques: [ + new TableUnique({ + columnNames: ["role"], + }), ], }); @@ -45,8 +51,8 @@ export const user_table = new Table({ { name: "username", ...getTypeByORM("varchar"), isUnique: true }, { name: "firstname", ...getTypeByORM("varchar") }, { name: "lastname", ...getTypeByORM("varchar") }, - { name: "secret", ...getTypeByORM("varchar") }, - { name: "static", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) }, + { name: "secret", ...getTypeByORM("text") }, + { name: "routine", ...getTypeByORM("varchar"), default: getDefaultByORM("string", LoginRoutineEnum.totp) }, { name: "isOwner", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) }, ], }); @@ -73,6 +79,14 @@ export const user_roles_table = new Table({ onUpdate: "RESTRICT", }), ], + indices: [ + new TableIndex({ + columnNames: ["userId"], + }), + new TableIndex({ + columnNames: ["roleId"], + }), + ], }); export const user_permission_table = new Table({ @@ -114,11 +128,19 @@ export const webapi_table = new Table({ name: "webapi", columns: [ { name: "id", ...getTypeByORM("int"), ...isIncrementPrimary }, - { name: "token", ...getTypeByORM("varchar"), isUnique: true }, - { name: "title", ...getTypeByORM("varchar"), isUnique: true }, + { name: "token", ...getTypeByORM("text") }, + { name: "title", ...getTypeByORM("varchar") }, { name: "createdAt", ...getTypeByORM("datetime", false, 6), default: getDefaultByORM("currentTimestamp", 6) }, - { name: "lastUsage", ...getTypeByORM("datetime", true, 6), default: getDefaultByORM("null") }, - { name: "expiry", ...getTypeByORM("date", true), default: getDefaultByORM("null") }, + { name: "lastUsage", ...getTypeByORM("datetime", true, 6) }, + { name: "expiry", ...getTypeByORM("date", true) }, + ], + uniques: [ + new TableUnique({ + columnNames: ["token"], + }), + new TableUnique({ + columnNames: ["title"], + }), ], }); @@ -148,3 +170,12 @@ export const reset_table = new Table({ { name: "secret", ...getTypeByORM("varchar") }, ], }); + +export const setting_table = new Table({ + name: "setting", + columns: [ + { name: "topic", ...getTypeByORM("varchar"), isPrimary: true }, + { name: "key", ...getTypeByORM("varchar"), isPrimary: true }, + { name: "value", ...getTypeByORM("text") }, + ], +}); diff --git a/src/migrations/baseSchemaTables/calendar.ts b/src/migrations/baseSchemaTables/calendar.ts index 155dc46..8d6e758 100644 --- a/src/migrations/baseSchemaTables/calendar.ts +++ b/src/migrations/baseSchemaTables/calendar.ts @@ -1,4 +1,4 @@ -import { Table, TableForeignKey } from "typeorm"; +import { Table, TableForeignKey, TableUnique } from "typeorm"; import { getDefaultByORM, getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "../ormHelper"; export const calendar_type_table = new Table({ @@ -32,7 +32,7 @@ export const calendar_table = new Table({ default: getDefaultByORM("currentTimestamp", 6), onUpdate: getDefaultByORM("currentTimestamp", 6), }, - { name: "webpageId", ...getTypeByORM("varchar", true), default: getDefaultByORM("null"), isUnique: true }, + { name: "webpageId", ...getTypeByORM("varchar", true) }, { name: "typeId", ...getTypeByORM("int") }, ], foreignKeys: [ @@ -44,4 +44,9 @@ export const calendar_table = new Table({ onUpdate: "RESTRICT", }), ], + uniques: [ + new TableUnique({ + columnNames: ["webpageId"], + }), + ], }); diff --git a/src/migrations/baseSchemaTables/member.ts b/src/migrations/baseSchemaTables/member.ts index c5f304b..be77d69 100644 --- a/src/migrations/baseSchemaTables/member.ts +++ b/src/migrations/baseSchemaTables/member.ts @@ -1,4 +1,4 @@ -import { Table, TableForeignKey, View } from "typeorm"; +import { Table, TableForeignKey, TableUnique, View } from "typeorm"; import { getDefaultByORM, getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "../ormHelper"; export const salutation_table = new Table({ @@ -51,17 +51,28 @@ export const qualification_table = new Table({ ], }); +export const education_table = new Table({ + name: "education", + columns: [ + { name: "id", ...getTypeByORM("int"), ...isIncrementPrimary }, + { name: "education", ...getTypeByORM("varchar"), isUnique: true }, + { name: "description", ...getTypeByORM("varchar"), isNullable: true }, + ], +}); + /** member and relations */ export const member_table = new Table({ name: "member", columns: [ { name: "id", ...getTypeByORM("uuid"), ...isUUIDPrimary }, { name: "salutationId", ...getTypeByORM("int") }, - { name: "internalId", ...getTypeByORM("varchar", true), default: getDefaultByORM("null"), isUnique: true }, + { name: "internalId", ...getTypeByORM("varchar", true) }, { name: "firstname", ...getTypeByORM("varchar") }, { name: "lastname", ...getTypeByORM("varchar") }, { name: "nameaffix", ...getTypeByORM("varchar") }, { name: "birthdate", ...getTypeByORM("date") }, + { name: "createdAt", ...getTypeByORM("datetime", false, 6), default: getDefaultByORM("currentTimestamp", 6) }, + { name: "note", ...getTypeByORM("varchar", true) }, ], foreignKeys: [ new TableForeignKey({ @@ -72,6 +83,11 @@ export const member_table = new Table({ onUpdate: "RESTRICT", }), ], + uniques: [ + new TableUnique({ + columnNames: ["internalId"], + }), + ], }); export const membership_table = new Table({ @@ -163,7 +179,7 @@ export const member_awards_table = new Table({ name: "member_awards", columns: [ { name: "id", ...getTypeByORM("int"), ...isIncrementPrimary }, - { name: "given", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) }, + { name: "given", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", true) }, { name: "note", ...getTypeByORM("varchar"), isNullable: true }, { name: "date", ...getTypeByORM("date") }, { name: "memberId", ...getTypeByORM("uuid") }, @@ -194,13 +210,13 @@ export const member_communication_table = new Table({ { name: "preferred", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) }, { name: "isSendNewsletter", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) }, { name: "isSMSAlarming", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) }, - { name: "mobile", ...getTypeByORM("varchar"), isNullable: true }, - { name: "email", ...getTypeByORM("varchar"), isNullable: true }, - { name: "postalCode", ...getTypeByORM("varchar"), default: getDefaultByORM("null"), isNullable: true }, - { name: "city", ...getTypeByORM("varchar"), isNullable: true }, - { name: "street", ...getTypeByORM("varchar"), isNullable: true }, + { name: "mobile", ...getTypeByORM("varchar", true) }, + { name: "email", ...getTypeByORM("varchar", true) }, + { name: "postalCode", ...getTypeByORM("varchar", true) }, + { name: "city", ...getTypeByORM("varchar", true) }, + { name: "street", ...getTypeByORM("varchar", true) }, { name: "streetNumber", ...getTypeByORM("int", true) }, - { name: "streetNumberAddition", ...getTypeByORM("varchar"), isNullable: true }, + { name: "streetNumberAddition", ...getTypeByORM("varchar", true) }, { name: "memberId", ...getTypeByORM("uuid") }, { name: "typeId", ...getTypeByORM("int") }, ], @@ -222,108 +238,60 @@ export const member_communication_table = new Table({ ], }); -/** views */ -export const member_view_mysql = new View({ - name: "member_view", - expression: ` - SELECT - \`member\`.\`id\` AS \`id\`, - \`member\`.\`internalId\` AS \`internalId\`, - \`member\`.\`firstname\` AS \`firstname\`, - \`member\`.\`lastname\` AS \`lastname\`, - \`member\`.\`nameaffix\` AS \`nameaffix\`, - \`member\`.\`birthdate\` AS \`birthdate\`, - \`salutation\`.\`salutation\` AS \`salutation\`, - TIMESTAMPDIFF(YEAR, \`member\`.\`birthdate\`, CURDATE()) AS \`todayAge\`, - YEAR(CURDATE()) - YEAR(\`member\`.\`birthdate\`) AS \`ageThisYear\`, - CONCAT( - TIMESTAMPDIFF(YEAR, \`member\`.\`birthdate\`, CURDATE()), ' years ', - TIMESTAMPDIFF(MONTH, \`member\`.\`birthdate\`, CURDATE()) % 12, ' months ', - TIMESTAMPDIFF(DAY, - DATE_ADD( - \`member\`.\`birthdate\`, - INTERVAL TIMESTAMPDIFF(MONTH, \`member\`.\`birthdate\`, CURDATE()) MONTH - ), - CURDATE() - ), ' days' - ) AS \`exactAge\` - FROM \`member\` \`member\` - LEFT JOIN \`salutation\` \`salutation\` ON \`salutation\`.\`id\`=\`member\`.\`salutationId\` - `, +export const member_educations_table = new Table({ + name: "member_educations", + columns: [ + { name: "id", ...getTypeByORM("int"), ...isIncrementPrimary }, + { name: "start", ...getTypeByORM("date") }, + { name: "end", ...getTypeByORM("date", true), default: getDefaultByORM("null") }, + { name: "note", ...getTypeByORM("varchar"), isNullable: true }, + { name: "place", ...getTypeByORM("varchar"), isNullable: true }, + { name: "memberId", ...getTypeByORM("uuid") }, + { name: "educationId", ...getTypeByORM("int") }, + ], + foreignKeys: [ + new TableForeignKey({ + columnNames: ["memberId"], + referencedTableName: "member", + referencedColumnNames: ["id"], + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }), + new TableForeignKey({ + columnNames: ["educationId"], + referencedTableName: "education", + referencedColumnNames: ["id"], + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }), + ], }); -export const member_view_postgres = new View({ +/** views */ + +export const member_view = new View({ name: "member_view", expression: ` SELECT "member"."id" AS "id", - "member"."internalId" AS "internalId", "member"."firstname" AS "firstname", "member"."lastname" AS "lastname", "member"."nameaffix" AS "nameaffix", "member"."birthdate" AS "birthdate", + "member"."internalId" AS "internalId", + "member"."note" AS "note", "salutation"."salutation" AS "salutation", - DATE_PART('year', AGE(CURRENT_DATE, member.birthdate)) AS "todayAge", - EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM member.birthdate) AS "ageThisYear", - AGE(CURRENT_DATE, member.birthdate) AS "exactAge" + DATE_PART('year', AGE(CURRENT_DATE, "member"."birthdate")) AS "todayAge", + EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM "member"."birthdate") AS "ageThisYear", + AGE(CURRENT_DATE, "member"."birthdate") AS "exactAge" FROM "member" "member" LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId" - `, + ` + .replace(/\s+/g, " ") + .trim(), }); -export const member_view_sqlite = new View({ - name: "member_view", - expression: ` - SELECT - member.id AS id, - member.internalId AS internalId, - member.firstname AS firstname, - member.lastname AS lastname, - member.nameaffix AS nameaffix, - member.birthdate AS birthdate, - salutation.salutation AS salutation, - (strftime('%Y', 'now') - strftime('%Y', member.birthdate) - (strftime('%m-%d', 'now') < strftime('%m-%d', member.birthdate))) AS todayAge, - FLOOR(strftime('%Y', 'now') - strftime('%Y', member.birthdate)) AS ageThisYear, - (strftime('%Y', 'now') - strftime('%Y', member.birthdate)) || ' years ' || - (strftime('%m', 'now') - strftime('%m', member.birthdate)) || ' months ' || - (strftime('%d', 'now') - strftime('%d', member.birthdate)) || ' days' - AS exactAge - FROM member member - LEFT JOIN salutation salutation ON salutation.id=member.salutationId - `, -}); - -export const member_executive_positions_view_mysql = new View({ - name: "member_executive_positions_view", - expression: ` - SELECT - \`executivePosition\`.\`id\` AS \`positionId\`, - \`executivePosition\`.\`position\` AS \`position\`, - \`member\`.\`id\` AS \`memberId\`, - \`member\`.\`firstname\` AS \`memberFirstname\`, - \`member\`.\`lastname\` AS \`memberLastname\`, - \`member\`.\`nameaffix\` AS \`memberNameaffix\`, - \`member\`.\`birthdate\` AS \`memberBirthdate\`, - \`salutation\`.\`salutation\` AS \`memberSalutation\`, - SUM(DATEDIFF(COALESCE(\`memberExecutivePositions\`.\`end\`, CURDATE()), \`memberExecutivePositions\`.\`start\`)) AS \`durationInDays\`, - SUM(TIMESTAMPDIFF(YEAR, \`memberExecutivePositions\`.\`start\`, COALESCE(\`memberExecutivePositions\`.\`end\`, CURDATE()))) AS \`durationInYears\`, - CONCAT( - SUM(FLOOR(TIMESTAMPDIFF(DAY, \`memberExecutivePositions\`.\`start\`, COALESCE(\`memberExecutivePositions\`.\`end\`, CURDATE())) / 365.25)), - ' years ', - SUM(FLOOR(MOD(TIMESTAMPDIFF(MONTH, \`memberExecutivePositions\`.\`start\`, COALESCE(\`memberExecutivePositions\`.\`end\`, CURDATE())), 12))), - ' months ', - SUM(FLOOR(MOD(TIMESTAMPDIFF(DAY, \`memberExecutivePositions\`.\`start\`, COALESCE(\`memberExecutivePositions\`.\`end\`, CURDATE())), 30))), - ' days' - ) AS \`exactDuration\` - FROM \`member_executive_positions\` \`memberExecutivePositions\` - LEFT JOIN \`executive_position\` \`executivePosition\` ON \`executivePosition\`.\`id\`=\`memberExecutivePositions\`.\`executivePositionId\` - LEFT JOIN \`member\` \`member\` ON \`member\`.\`id\`=\`memberExecutivePositions\`.\`memberId\` - LEFT JOIN \`salutation\` \`salutation\` ON \`salutation\`.\`id\`=\`member\`.\`salutationId\` - GROUP BY \`executivePosition\`.\`id\`, \`member\`.\`id\`, \`salutation\`.\`id\` - `, -}); - -export const member_executive_positions_view_postgres = new View({ +export const member_executive_positions_view = new View({ name: "member_executive_positions_view", expression: ` SELECT @@ -343,66 +311,12 @@ export const member_executive_positions_view_postgres = new View({ LEFT JOIN "member" "member" ON "member"."id"="memberExecutivePositions"."memberId" LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId" GROUP BY "executivePosition"."id", "member"."id", "salutation"."id" - `, + ` + .replace(/\s+/g, " ") + .trim(), }); -export const member_executive_positions_view_sqlite = new View({ - name: "member_executive_positions_view", - expression: ` - SELECT - executivePosition.id AS positionId, - executivePosition.position AS position, - member.id AS memberId, - member.firstname AS memberFirstname, - member.lastname AS memberLastname, - member.nameaffix AS memberNameaffix, - member.birthdate AS memberBirthdate, - salutation.salutation AS memberSalutation, - SUM(JULIANDAY(COALESCE(memberExecutivePositions.end, DATE('now'))) - JULIANDAY(memberExecutivePositions.start)) AS durationInDays, - SUM(FLOOR((JULIANDAY(COALESCE(memberExecutivePositions.end, DATE('now'))) - JULIANDAY(memberExecutivePositions.start)) / 365.25)) AS durationInYears, - SUM((strftime('%Y', COALESCE(memberExecutivePositions.end, DATE('now'))) - strftime('%Y', memberExecutivePositions.start))) || ' years ' || - SUM((strftime('%m', COALESCE(memberExecutivePositions.end, DATE('now'))) - strftime('%m', memberExecutivePositions.start))) || ' months ' || - SUM((strftime('%d', COALESCE(memberExecutivePositions.end, DATE('now'))) - strftime('%d', memberExecutivePositions.start))) || ' days' - AS exactDuration - FROM member_executive_positions memberExecutivePositions - LEFT JOIN executive_position executivePosition ON executivePosition.id=memberExecutivePositions.executivePositionId - LEFT JOIN member member ON member.id=memberExecutivePositions.memberId - LEFT JOIN salutation salutation ON salutation.id=member.salutationId - GROUP BY executivePosition.id, member.id, salutation.id - `, -}); - -export const member_qualifications_view_mysql = new View({ - name: "member_qualifications_view", - expression: ` - SELECT - \`qualification\`.\`id\` AS \`qualificationId\`, - \`qualification\`.\`qualification\` AS \`qualification\`, - \`member\`.\`id\` AS \`memberId\`, - \`member\`.\`firstname\` AS \`memberFirstname\`, - \`member\`.\`lastname\` AS \`memberLastname\`, - \`member\`.\`nameaffix\` AS \`memberNameaffix\`, - \`member\`.\`birthdate\` AS \`memberBirthdate\`, - \`salutation\`.\`salutation\` AS \`memberSalutation\`, - SUM(DATEDIFF(COALESCE(\`memberQualifications\`.\`end\`, CURDATE()), \`memberQualifications\`.\`start\`)) AS \`durationInDays\`, - SUM(TIMESTAMPDIFF(YEAR, \`memberQualifications\`.\`start\`, COALESCE(\`memberQualifications\`.\`end\`, CURDATE()))) AS \`durationInYears\`, - CONCAT( - SUM(FLOOR(TIMESTAMPDIFF(DAY, \`memberQualifications\`.\`start\`, COALESCE(\`memberQualifications\`.\`end\`, CURDATE())) / 365.25)), - ' years ', - SUM(FLOOR(MOD(TIMESTAMPDIFF(MONTH, \`memberQualifications\`.\`start\`, COALESCE(\`memberQualifications\`.\`end\`, CURDATE())), 12))), - ' months ', - SUM(FLOOR(MOD(TIMESTAMPDIFF(DAY, \`memberQualifications\`.\`start\`, COALESCE(\`memberQualifications\`.\`end\`, CURDATE())), 30))), - ' days' - ) AS \`exactDuration\` - FROM \`member_qualifications\` \`memberQualifications\` - LEFT JOIN \`qualification\` \`qualification\` ON \`qualification\`.\`id\`=\`memberQualifications\`.\`qualificationId\` - LEFT JOIN \`member\` \`member\` ON \`member\`.\`id\`=\`memberQualifications\`.\`memberId\` - LEFT JOIN \`salutation\` \`salutation\` ON \`salutation\`.\`id\`=\`member\`.\`salutationId\` - GROUP BY \`qualification\`.\`id\`, \`member\`.\`id\`, \`salutation\`.\`id\` - `, -}); - -export const member_qualifications_view_postgres = new View({ +export const member_qualifications_view = new View({ name: "member_qualifications_view", expression: ` SELECT @@ -422,66 +336,12 @@ export const member_qualifications_view_postgres = new View({ LEFT JOIN "member" "member" ON "member"."id"="memberQualifications"."memberId" LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId" GROUP BY "qualification"."id", "member"."id", "salutation"."id" - `, + ` + .replace(/\s+/g, " ") + .trim(), }); -export const member_qualifications_view_sqlite = new View({ - name: "member_qualifications_view", - expression: ` - SELECT - qualification.id AS qualificationId, - qualification.qualification AS qualification, - member.id AS memberId, - member.firstname AS memberFirstname, - member.lastname AS memberLastname, - member.nameaffix AS memberNameaffix, - member.birthdate AS memberBirthdate, - salutation.salutation AS memberSalutation, - SUM(JULIANDAY(COALESCE(memberQualifications.end, DATE('now'))) - JULIANDAY(memberQualifications.start)) AS durationInDays, - SUM(FLOOR((JULIANDAY(COALESCE(memberQualifications.end, DATE('now'))) - JULIANDAY(memberQualifications.start)) / 365.25)) AS durationInYears, - SUM((strftime('%Y', COALESCE(memberQualifications.end, DATE('now'))) - strftime('%Y', memberQualifications.start))) || ' years ' || - SUM((strftime('%m', COALESCE(memberQualifications.end, DATE('now'))) - strftime('%m', memberQualifications.start))) || ' months ' || - SUM((strftime('%d', COALESCE(memberQualifications.end, DATE('now'))) - strftime('%d', memberQualifications.start))) || ' days' - AS exactDuration - FROM member_qualifications memberQualifications - LEFT JOIN qualification qualification ON qualification.id=memberQualifications.qualificationId - LEFT JOIN member member ON member.id=memberQualifications.memberId - LEFT JOIN salutation salutation ON salutation.id=member.salutationId - GROUP BY qualification.id, member.id, salutation.id - `, -}); - -export const membership_view_mysql = new View({ - name: "membership_view", - expression: ` - SELECT - \`status\`.\`id\` AS \`statusId\`, - \`status\`.\`status\` AS \`status\`, - \`member\`.\`id\` AS \`memberId\`, - \`member\`.\`firstname\` AS \`memberFirstname\`, - \`member\`.\`lastname\` AS \`memberLastname\`, - \`member\`.\`nameaffix\` AS \`memberNameaffix\`, - \`member\`.\`birthdate\` AS \`memberBirthdate\`, - \`salutation\`.\`salutation\` AS \`memberSalutation\`, - SUM(DATEDIFF(COALESCE(\`membership\`.\`end\`, CURDATE()), \`membership\`.\`start\`)) AS \`durationInDays\`, - SUM(TIMESTAMPDIFF(YEAR, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE()))) AS \`durationInYears\`, - CONCAT( - SUM(FLOOR(TIMESTAMPDIFF(DAY, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())) / 365.25)), - ' years ', - SUM(FLOOR(MOD(TIMESTAMPDIFF(MONTH, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())), 12))), - ' months ', - SUM(FLOOR(MOD(TIMESTAMPDIFF(DAY, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())), 30))), - ' days' - ) AS \`exactDuration\` - FROM \`membership\` \`membership\` - LEFT JOIN \`membership_status\` \`status\` ON \`status\`.\`id\`=\`membership\`.\`statusId\` - LEFT JOIN \`member\` \`member\` ON \`member\`.\`id\`=\`membership\`.\`memberId\` - LEFT JOIN \`salutation\` \`salutation\` ON \`salutation\`.\`id\`=\`member\`.\`salutationId\` - GROUP BY \`status\`.\`id\`, \`member\`.\`id\`, \`salutation\`.\`id\` - `, -}); - -export const membership_view_postgres = new View({ +export const membership_view = new View({ name: "membership_view", expression: ` SELECT @@ -500,32 +360,31 @@ export const membership_view_postgres = new View({ LEFT JOIN "membership_status" "status" ON "status"."id"="membership"."statusId" LEFT JOIN "member" "member" ON "member"."id"="membership"."memberId" LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId" - GROUP BY "status"."id","member"."id", "salutation"."id" - `, + GROUP BY "status"."id", "member"."id", "salutation"."id" + ` + .replace(/\s+/g, " ") + .trim(), }); -export const membership_view_sqlite = new View({ - name: "membership_view", +export const membership_total_view = new View({ + name: "membership_total_view", expression: ` - SELECT - status.id AS statusId, - status.status AS status, - member.id AS memberId, - member.firstname AS memberFirstname, - member.lastname AS memberLastname, - member.nameaffix AS memberNameaffix, - member.birthdate AS memberBirthdate, - salutation.salutation AS memberSalutation, - SUM(JULIANDAY(COALESCE(membership.end, DATE('now'))) - JULIANDAY(membership.start)) AS durationInDays, - SUM(FLOOR((JULIANDAY(COALESCE(membership.end, DATE('now'))) - JULIANDAY(membership.start)) / 365.25)) AS durationInYears, - SUM((strftime('%Y', COALESCE(membership.end, DATE('now'))) - strftime('%Y', membership.start))) || ' years ' || - SUM((strftime('%m', COALESCE(membership.end, DATE('now'))) - strftime('%m', membership.start))) || ' months ' || - SUM((strftime('%d', COALESCE(membership.end, DATE('now'))) - strftime('%d', membership.start))) || ' days' - AS exactDuration - FROM membership membership - LEFT JOIN membership_status status ON status.id=membership.statusId - LEFT JOIN member member ON member.id=membership.memberId - LEFT JOIN salutation salutation ON salutation.id=member.salutationId - GROUP BY status.id, member.id, salutation.id - `, + SELECT + "member"."id" AS "memberId", + "member"."firstname" AS "memberFirstname", + "member"."lastname" AS "memberLastname", + "member"."nameaffix" AS "memberNameaffix", + "member"."birthdate" AS "memberBirthdate", + "salutation"."salutation" AS "memberSalutation", + SUM(COALESCE("membership"."end", CURRENT_DATE) - "membership"."start") AS "durationInDays", + SUM(EXTRACT(YEAR FROM AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start"))) AS "durationInYears", + SUM(AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start")) AS "exactDuration" + FROM "membership" "membership" + LEFT JOIN "membership_status" "status" ON "status"."id"="membership"."statusId" + LEFT JOIN "member" "member" ON "member"."id"="membership"."memberId" + LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId" + GROUP BY "member"."id", "salutation"."id" + ` + .replace(/\s+/g, " ") + .trim(), }); diff --git a/src/migrations/baseSchemaTables/newsletter.ts b/src/migrations/baseSchemaTables/newsletter.ts index 537428f..2141cc7 100644 --- a/src/migrations/baseSchemaTables/newsletter.ts +++ b/src/migrations/baseSchemaTables/newsletter.ts @@ -1,4 +1,4 @@ -import { Table, TableForeignKey } from "typeorm"; +import { Table, TableForeignKey, TableUnique } from "typeorm"; import { getDefaultByORM, getTypeByORM, isIncrementPrimary } from "../ormHelper"; export const newsletter_table = new Table({ @@ -7,11 +7,12 @@ export const newsletter_table = new Table({ { name: "id", ...getTypeByORM("int"), ...isIncrementPrimary }, { name: "title", ...getTypeByORM("varchar") }, { name: "description", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, - { name: "newsletterTitle", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, + { name: "newsletterTitle", ...getTypeByORM("text"), default: getDefaultByORM("string") }, { name: "newsletterText", ...getTypeByORM("text"), default: getDefaultByORM("string") }, - { name: "newsletterSignatur", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, + { name: "newsletterSignatur", ...getTypeByORM("text"), default: getDefaultByORM("string") }, { name: "isSent", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) }, - { name: "recipientsByQueryId", ...getTypeByORM("int", true) }, + { name: "recipientsByQueryId", ...getTypeByORM("uuid", true) }, + { name: "createdAt", ...getTypeByORM("datetime", false, 6), default: getDefaultByORM("currentTimestamp", 6) }, ], foreignKeys: [ new TableForeignKey({ @@ -22,6 +23,11 @@ export const newsletter_table = new Table({ onUpdate: "RESTRICT", }), ], + uniques: [ + new TableUnique({ + columnNames: ["title"], + }), + ], }); export const newsletter_dates_table = new Table({ diff --git a/src/migrations/baseSchemaTables/protocol.ts b/src/migrations/baseSchemaTables/protocol.ts index fd1cc89..c04fca3 100644 --- a/src/migrations/baseSchemaTables/protocol.ts +++ b/src/migrations/baseSchemaTables/protocol.ts @@ -1,11 +1,11 @@ -import { Table, TableForeignKey } from "typeorm"; +import { Table, TableForeignKey, TableUnique } from "typeorm"; import { getDefaultByORM, getTypeByORM, isIncrementPrimary } from "../ormHelper"; export const protocol_table = new Table({ name: "protocol", columns: [ { name: "id", ...getTypeByORM("int"), ...isIncrementPrimary }, - { name: "title", ...getTypeByORM("varchar") }, + { name: "title", ...getTypeByORM("varchar"), isUnique: true }, { name: "date", ...getTypeByORM("date") }, { name: "starttime", ...getTypeByORM("time", true) }, { name: "endtime", ...getTypeByORM("time", true) }, @@ -19,6 +19,7 @@ export const protocol_agenda_table = new Table({ { name: "id", ...getTypeByORM("int"), ...isIncrementPrimary }, { name: "topic", ...getTypeByORM("varchar") }, { name: "context", ...getTypeByORM("text"), default: getDefaultByORM("string") }, + { name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }, { name: "protocolId", ...getTypeByORM("int") }, ], foreignKeys: [ @@ -38,6 +39,7 @@ export const protocol_decision_table = new Table({ { name: "id", ...getTypeByORM("int"), ...isIncrementPrimary }, { name: "topic", ...getTypeByORM("varchar") }, { name: "context", ...getTypeByORM("text"), default: getDefaultByORM("string") }, + { name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }, { name: "protocolId", ...getTypeByORM("int") }, ], foreignKeys: [ @@ -86,6 +88,7 @@ export const protocol_voting_table = new Table({ { name: "favour", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }, { name: "abstain", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }, { name: "against", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }, + { name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }, { name: "protocolId", ...getTypeByORM("int") }, ], foreignKeys: [ diff --git a/src/migrations/baseSchemaTables/query_template.ts b/src/migrations/baseSchemaTables/query_template.ts index da4eb44..2d8c4c8 100644 --- a/src/migrations/baseSchemaTables/query_template.ts +++ b/src/migrations/baseSchemaTables/query_template.ts @@ -1,12 +1,18 @@ import { Table, TableForeignKey } from "typeorm"; -import { getDefaultByORM, getTypeByORM, isIncrementPrimary } from "../ormHelper"; +import { getDefaultByORM, getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "../ormHelper"; export const query_table = new Table({ name: "query", columns: [ - { name: "id", ...getTypeByORM("int"), ...isIncrementPrimary }, + { name: "id", ...getTypeByORM("uuid"), ...isUUIDPrimary }, { name: "title", ...getTypeByORM("varchar"), isUnique: true }, { name: "query", ...getTypeByORM("text"), default: getDefaultByORM("string") }, + { + name: "updatedAt", + ...getTypeByORM("datetime", false, 6), + default: getDefaultByORM("currentTimestamp", 6), + onUpdate: getDefaultByORM("currentTimestamp", 6), + }, ], }); diff --git a/src/migrations/ormHelper.ts b/src/migrations/ormHelper.ts index 20a57ad..c376f2b 100644 --- a/src/migrations/ormHelper.ts +++ b/src/migrations/ormHelper.ts @@ -13,83 +13,38 @@ export type Primary = { }; export function getTypeByORM(type: ORMType, nullable: boolean = false, length: number = 255): ColumnConfig { - const dbType = process.env.DB_TYPE; - - const typeMap: Record> = { - mysql: { - int: "int", - bigint: "bigint", - boolean: "tinyint", - date: "date", - datetime: "datetime", - time: "time", - text: "text", - varchar: "varchar", - uuid: "varchar", - }, - postgres: { - int: "integer", - bigint: "bigint", - boolean: "boolean", - date: "date", - datetime: "timestamp", - time: "time", - text: "text", - varchar: "character varying", - uuid: "uuid", - }, - sqlite: { - int: "integer", - bigint: "integer", - boolean: "integer", - date: "date", - datetime: "datetime", - time: "text", - text: "text", - varchar: "varchar", - uuid: "varchar", - }, + const typeMap: Record = { + int: "integer", + bigint: "bigint", + boolean: "boolean", + date: "date", + datetime: "timestamp", + time: "time", + text: "text", + varchar: "character varying", + uuid: "uuid", }; let obj: ColumnConfig = { - type: typeMap[dbType]?.[type] || type, + type: typeMap[type] || type, isNullable: nullable, }; if (type == "datetime") obj.precision = length; - else if (dbType != "sqlite" && (obj.type == "varchar" || type == "varchar")) obj.length = `${length}`; - else if (dbType != "postgres" && type == "uuid") obj.length = "36"; + else if (obj.type == "varchar" || type == "varchar") obj.length = `${length}`; return obj; } export function getDefaultByORM(type: ORMDefault, data?: string | number | boolean): T { - const dbType = process.env.DB_TYPE; - - const typeMap: Record> = { - mysql: { - currentTimestamp: `CURRENT_TIMESTAMP(${data ?? 6})`, - string: `'${data ?? ""}'`, - boolean: Boolean(data).toString(), - number: Number(data).toString(), - null: null, - }, - postgres: { - currentTimestamp: `now()`, - string: `'${data ?? ""}'`, - boolean: Boolean(data) == true ? "true" : "false", - number: Number(data).toString(), - null: null, - }, - sqlite: { - currentTimestamp: `datetime('now')`, - string: `'${data ?? ""}'`, - boolean: Boolean(data) == true ? "1" : "0", - number: Number(data).toString(), - null: null, - }, + const typeMap: Record = { + currentTimestamp: `now()`, + string: `'${data ?? ""}'`, + boolean: Boolean(data) == true ? "true" : "false", + number: Number(data).toString(), + null: null, }; - return (typeMap[dbType]?.[type] || type) as T; + return (typeMap[type] || type) as T; } export const isIncrementPrimary: Primary = { diff --git a/src/routes/admin/club/member.ts b/src/routes/admin/club/member.ts index efd6e05..4ceb5f5 100644 --- a/src/routes/admin/club/member.ts +++ b/src/routes/admin/club/member.ts @@ -2,12 +2,14 @@ import express, { Request, Response } from "express"; import { addAwardToMember, addCommunicationToMember, + addEducationToMember, addExecutivePositionToMember, addMembershipToMember, addQualificationToMember, createMember, deleteAwardOfMember, deleteCommunicationOfMember, + deleteEducationsOfMember, deleteExecutivePositionOfMember, deleteMemberById, deleteMembershipOfMember, @@ -17,19 +19,24 @@ import { getAwardsByMember, getCommunicationByMemberAndRecord, getCommunicationsByMember, + getEducationByMemberAndRecord, + getEducationsByMember, getExecutivePositionByMemberAndRecord, getExecutivePositionsByMember, getMemberById, + getMemberLastInternalId, getMemberPrintoutById, getMembersByIds, getMembershipByMemberAndRecord, getMembershipsByMember, getMembershipStatisticsById, + getMembershipTotalStatisticsById, getMemberStatisticsById, getQualificationByMemberAndRecord, getQualificationsByMember, updateAwardOfMember, updateCommunicationOfMember, + updateEducationOfMember, updateExecutivePositionOfMember, updateMemberById, updateMembershipOfMember, @@ -43,6 +50,10 @@ router.get("/", async (req: Request, res: Response) => { await getAllMembers(req, res); }); +router.get("/last/internalId", async (req: Request, res: Response) => { + await getMemberLastInternalId(req, res); +}); + router.post("/ids", async (req: Request, res: Response) => { await getMembersByIds(req, res); }); @@ -67,6 +78,10 @@ router.get("/:memberId/memberships/statistics", async (req: Request, res: Respon await getMembershipStatisticsById(req, res); }); +router.get("/:memberId/memberships/totalstatistics", async (req: Request, res: Response) => { + await getMembershipTotalStatisticsById(req, res); +}); + router.get("/:memberId/membership/:id", async (req: Request, res: Response) => { await getMembershipByMemberAndRecord(req, res); }); @@ -87,6 +102,14 @@ router.get("/:memberId/qualification/:id", async (req: Request, res: Response) = await getQualificationByMemberAndRecord(req, res); }); +router.get("/:memberId/educations", async (req: Request, res: Response) => { + await getEducationsByMember(req, res); +}); + +router.get("/:memberId/education/:id", async (req: Request, res: Response) => { + await getEducationByMemberAndRecord(req, res); +}); + router.get("/:memberId/positions", async (req: Request, res: Response) => { await getExecutivePositionsByMember(req, res); }); @@ -135,6 +158,14 @@ router.post( } ); +router.post( + "/:memberId/education", + PermissionHelper.passCheckMiddleware("create", "club", "member"), + async (req: Request, res: Response) => { + await addEducationToMember(req, res); + } +); + router.post( "/:memberId/position", PermissionHelper.passCheckMiddleware("create", "club", "member"), @@ -183,6 +214,14 @@ router.patch( } ); +router.patch( + "/:memberId/education/:recordId", + PermissionHelper.passCheckMiddleware("update", "club", "member"), + async (req: Request, res: Response) => { + await updateEducationOfMember(req, res); + } +); + router.patch( "/:memberId/position/:recordId", PermissionHelper.passCheckMiddleware("update", "club", "member"), @@ -231,6 +270,14 @@ router.delete( } ); +router.delete( + "/:memberId/education/:recordId", + PermissionHelper.passCheckMiddleware("delete", "club", "member"), + async (req: Request, res: Response) => { + await deleteEducationsOfMember(req, res); + } +); + router.delete( "/:memberId/position/:recordId", PermissionHelper.passCheckMiddleware("delete", "club", "member"), diff --git a/src/routes/admin/configuration/education.ts b/src/routes/admin/configuration/education.ts new file mode 100644 index 0000000..8a07628 --- /dev/null +++ b/src/routes/admin/configuration/education.ts @@ -0,0 +1,45 @@ +import express, { Request, Response } from "express"; +import { + createEducation, + deleteEducation, + getAllEducations, + getEducationById, + updateEducation, +} from "../../../controller/admin/configuration/educationController"; +import PermissionHelper from "../../../helpers/permissionHelper"; + +var router = express.Router({ mergeParams: true }); + +router.get("/", async (req: Request, res: Response) => { + await getAllEducations(req, res); +}); + +router.get("/:id", async (req: Request, res: Response) => { + await getEducationById(req, res); +}); + +router.post( + "/", + PermissionHelper.passCheckMiddleware("create", "configuration", "education"), + async (req: Request, res: Response) => { + await createEducation(req, res); + } +); + +router.patch( + "/:id", + PermissionHelper.passCheckMiddleware("update", "configuration", "education"), + async (req: Request, res: Response) => { + await updateEducation(req, res); + } +); + +router.delete( + "/:id", + PermissionHelper.passCheckMiddleware("delete", "configuration", "education"), + async (req: Request, res: Response) => { + await deleteEducation(req, res); + } +); + +export default router; diff --git a/src/routes/admin/index.ts b/src/routes/admin/index.ts index c2bab0f..b306bfc 100644 --- a/src/routes/admin/index.ts +++ b/src/routes/admin/index.ts @@ -7,6 +7,7 @@ import communicationType from "./configuration/communicationType"; import executivePosition from "./configuration/executivePosition"; import membershipStatus from "./configuration/membershipStatus"; import qualification from "./configuration/qualification"; +import education from "./configuration/education"; import salutation from "./configuration/salutation"; import calendarType from "./configuration/calendarType"; import queryStore from "./configuration/queryStore"; @@ -26,70 +27,79 @@ import user from "./management/user"; import invite from "./management/invite"; import api from "./management/webapi"; import backup from "./management/backup"; +import setting from "./management/setting"; var router = express.Router({ mergeParams: true }); router.use( "/award", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "configuration", module: "award" }, - { requiredPermissions: "read", section: "club", module: "member" }, + { requiredPermission: "read", section: "configuration", module: "award" }, + { requiredPermission: "read", section: "club", module: "member" }, ]), award ); router.use( "/communicationtype", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "configuration", module: "communication_type" }, - { requiredPermissions: "read", section: "club", module: "member" }, + { requiredPermission: "read", section: "configuration", module: "communication_type" }, + { requiredPermission: "read", section: "club", module: "member" }, ]), communicationType ); router.use( "/executiveposition", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "configuration", module: "executive_position" }, - { requiredPermissions: "read", section: "club", module: "member" }, + { requiredPermission: "read", section: "configuration", module: "executive_position" }, + { requiredPermission: "read", section: "club", module: "member" }, ]), executivePosition ); router.use( "/membershipstatus", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "configuration", module: "membership_status" }, - { requiredPermissions: "read", section: "club", module: "member" }, + { requiredPermission: "read", section: "configuration", module: "membership_status" }, + { requiredPermission: "read", section: "club", module: "member" }, ]), membershipStatus ); router.use( "/qualification", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "configuration", module: "qualification" }, - { requiredPermissions: "read", section: "club", module: "member" }, + { requiredPermission: "read", section: "configuration", module: "qualification" }, + { requiredPermission: "read", section: "club", module: "member" }, ]), qualification ); +router.use( + "/education", + PermissionHelper.passCheckSomeMiddleware([ + { requiredPermission: "read", section: "configuration", module: "education" }, + { requiredPermission: "read", section: "club", module: "member" }, + ]), + education +); router.use( "/salutation", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "configuration", module: "salutation" }, - { requiredPermissions: "read", section: "club", module: "member" }, + { requiredPermission: "read", section: "configuration", module: "salutation" }, + { requiredPermission: "read", section: "club", module: "member" }, ]), salutation ); router.use( "/calendartype", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "configuration", module: "calendar_type" }, - { requiredPermissions: "read", section: "club", module: "calendar" }, + { requiredPermission: "read", section: "configuration", module: "calendar_type" }, + { requiredPermission: "read", section: "club", module: "calendar" }, ]), calendarType ); router.use( "/querystore", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "configuration", module: "query_store" }, - { requiredPermissions: "read", section: "club", module: "listprint" }, + { requiredPermission: "read", section: "configuration", module: "query_store" }, + { requiredPermission: "read", section: "club", module: "listprint" }, ]), queryStore ); @@ -97,16 +107,16 @@ router.use("/template", PermissionHelper.passCheckMiddleware("read", "configurat router.use( "/templateusage", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "configuration", module: "template_usage" }, - { requiredPermissions: "read", section: "configuration", module: "template" }, + { requiredPermission: "read", section: "configuration", module: "template_usage" }, + { requiredPermission: "read", section: "configuration", module: "template" }, ]), templateUsage ); router.use( "/newsletterconfig", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "configuration", module: "newsletter_config" }, - { requiredPermissions: "read", section: "configuration", module: "communication_type" }, + { requiredPermission: "read", section: "configuration", module: "newsletter_config" }, + { requiredPermission: "read", section: "configuration", module: "communication_type" }, ]), newsletterConfig ); @@ -115,8 +125,8 @@ router.use("/member", PermissionHelper.passCheckMiddleware("read", "club", "memb router.use( "/protocol", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "club", module: "protocol" }, - { requiredPermissions: "read", section: "club", module: "member" }, + { requiredPermission: "read", section: "club", module: "protocol" }, + { requiredPermission: "read", section: "club", module: "member" }, ]), protocol ); @@ -124,19 +134,19 @@ router.use("/calendar", PermissionHelper.passCheckMiddleware("read", "club", "ca router.use( "/querybuilder", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "club", module: "query" }, - { requiredPermissions: "read", section: "configuration", module: "query_store" }, + { requiredPermission: "read", section: "club", module: "query" }, + { requiredPermission: "read", section: "configuration", module: "query_store" }, ]), queryBuilder ); router.use( "/newsletter", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "club", module: "newsletter" }, - { requiredPermissions: "read", section: "club", module: "member" }, - { requiredPermissions: "read", section: "club", module: "calendar" }, - { requiredPermissions: "read", section: "club", module: "query" }, - { requiredPermissions: "read", section: "configuration", module: "query_store" }, + { requiredPermission: "read", section: "club", module: "newsletter" }, + { requiredPermission: "read", section: "club", module: "member" }, + { requiredPermission: "read", section: "club", module: "calendar" }, + { requiredPermission: "read", section: "club", module: "query" }, + { requiredPermission: "read", section: "configuration", module: "query_store" }, ]), newsletter ); @@ -146,8 +156,8 @@ router.use("/role", PermissionHelper.passCheckMiddleware("read", "management", " router.use( "/user", PermissionHelper.passCheckSomeMiddleware([ - { requiredPermissions: "read", section: "management", module: "user" }, - { requiredPermissions: "read", section: "management", module: "role" }, + { requiredPermission: "read", section: "management", module: "user" }, + { requiredPermission: "read", section: "management", module: "role" }, ]), user ); @@ -159,5 +169,6 @@ router.use( PermissionHelper.passCheckMiddleware("read", "management", "backup"), backup ); +router.use("/setting", PermissionHelper.passCheckMiddleware("read", "management", "setting"), setting); export default router; diff --git a/src/routes/admin/management/setting.ts b/src/routes/admin/management/setting.ts new file mode 100644 index 0000000..c284a32 --- /dev/null +++ b/src/routes/admin/management/setting.ts @@ -0,0 +1,56 @@ +import express, { Request, Response } from "express"; +import PermissionHelper from "../../../helpers/permissionHelper"; +import { + getSetting, + getSettings, + resetSetting, + setImages, + setSetting, + setSettings, +} from "../../../controller/admin/management/settingController"; +import { clubImageUpload } from "../../../middleware/multer"; + +var router = express.Router({ mergeParams: true }); + +router.get("/", async (req: Request, res: Response) => { + await getSettings(req, res); +}); + +router.get("/:setting", async (req: Request, res: Response) => { + await getSetting(req, res); +}); + +router.put( + "/", + PermissionHelper.passCheckMiddleware("create", "management", "setting"), + async (req: Request, res: Response) => { + await setSetting(req, res); + } +); + +router.put( + "/multi", + PermissionHelper.passCheckMiddleware("create", "management", "setting"), + async (req: Request, res: Response) => { + await setSettings(req, res); + } +); + +router.put( + "/images", + PermissionHelper.passCheckMiddleware("create", "management", "setting"), + clubImageUpload, + async (req: Request, res: Response) => { + await setImages(req, res); + } +); + +router.delete( + "/:setting", + PermissionHelper.passCheckMiddleware("delete", "management", "setting"), + async (req: Request, res: Response) => { + await resetSetting(req, res); + } +); + +export default router; diff --git a/src/routes/admin/management/user.ts b/src/routes/admin/management/user.ts index 3a419d6..15d3e27 100644 --- a/src/routes/admin/management/user.ts +++ b/src/routes/admin/management/user.ts @@ -10,7 +10,6 @@ import { updateUserPermissions, updateUserRoles, } from "../../../controller/admin/management/userController"; -import { inviteUser } from "../../../controller/inviteController"; var router = express.Router({ mergeParams: true }); diff --git a/src/routes/auth.ts b/src/routes/auth.ts index b1200bc..c15f7cb 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -1,8 +1,12 @@ import express from "express"; -import { login, logout, refresh } from "../controller/authController"; +import { kickof, login, logout, refresh } from "../controller/authController"; var router = express.Router({ mergeParams: true }); +router.post("/kickof", async (req, res) => { + await kickof(req, res); +}); + router.post("/login", async (req, res) => { await login(req, res); }); diff --git a/src/routes/invite.ts b/src/routes/invite.ts index 783ecef..ebb4ddd 100644 --- a/src/routes/invite.ts +++ b/src/routes/invite.ts @@ -1,6 +1,5 @@ import express from "express"; -import { isSetup } from "../controller/setupController"; -import { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController"; +import { finishInvite, verifyInvite } from "../controller/inviteController"; import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper"; var router = express.Router({ mergeParams: true }); @@ -9,8 +8,12 @@ router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mai await verifyInvite(req, res); }); -router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => { - await finishInvite(req, res); -}); +router.put( + "/", + ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "secret", "routine "]), + async (req, res) => { + await finishInvite(req, res); + } +); export default router; diff --git a/src/routes/public.ts b/src/routes/public.ts index 56b4784..d557dd7 100644 --- a/src/routes/public.ts +++ b/src/routes/public.ts @@ -1,5 +1,12 @@ import express from "express"; -import { getCalendarItemsByTypes } from "../controller/publicController"; +import { + getApplicationConfig, + getApplicationFavicon, + getApplicationIcon, + getApplicationLogo, + getApplicationManifest, + getCalendarItemsByTypes, +} from "../controller/publicController"; var router = express.Router({ mergeParams: true }); @@ -7,4 +14,24 @@ router.get("/calendar", async (req, res) => { await getCalendarItemsByTypes(req, res); }); +router.get("/configuration", async (req, res) => { + await getApplicationConfig(req, res); +}); + +router.get("/manifest.webmanifest", async (req, res) => { + await getApplicationManifest(req, res); +}); + +router.get("/applogo.png", async (req, res) => { + await getApplicationLogo(req, res); +}); + +router.get("/favicon.ico", async (req, res) => { + await getApplicationFavicon(req, res); +}); + +router.get("/icon.png", async (req, res) => { + await getApplicationIcon(req, res); +}); + export default router; diff --git a/src/routes/reset.ts b/src/routes/reset.ts index acb1516..31df6c4 100644 --- a/src/routes/reset.ts +++ b/src/routes/reset.ts @@ -12,8 +12,12 @@ router.post("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["username" await startReset(req, res); }); -router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => { - await finishReset(req, res); -}); +router.put( + "/", + ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "secret", "routine"]), + async (req, res) => { + await finishReset(req, res); + } +); export default router; diff --git a/src/routes/server.ts b/src/routes/server.ts index 964d207..782609d 100644 --- a/src/routes/server.ts +++ b/src/routes/server.ts @@ -5,7 +5,7 @@ import Parser from "rss-parser"; var router = express.Router({ mergeParams: true }); router.get("/version", async (req: Request, res: Response) => { - let serverPackage = FileSystemHelper.readTemplateFile("/package.json"); + let serverPackage = FileSystemHelper.readRootFile("/package.json"); let serverJson = JSON.parse(serverPackage); res.send({ name: serverJson.name, diff --git a/src/routes/setup.ts b/src/routes/setup.ts index 159e04e..4830d4a 100644 --- a/src/routes/setup.ts +++ b/src/routes/setup.ts @@ -1,7 +1,14 @@ import express from "express"; -import { isSetup } from "../controller/setupController"; +import { + isSetup, + setAppIdentity, + setClubIdentity, + setMailConfig, + uploadClubImages, +} from "../controller/setupController"; import { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController"; import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper"; +import { clubImageUpload } from "../middleware/multer"; var router = express.Router({ mergeParams: true }); @@ -9,20 +16,44 @@ router.get("/", async (req, res) => { await isSetup(req, res); }); +router.post("/club", async (req, res) => { + await setClubIdentity(req, res); +}); + +router.post("/club/images", clubImageUpload, async (req, res) => { + await uploadClubImages(req, res); +}); + +router.post("/app", async (req, res) => { + await setAppIdentity(req, res); +}); + +router.post( + "/mail", + ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "username", "password", "host", "port", "secure"]), + async (req, res) => { + await setMailConfig(req, res); + } +); + router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => { await verifyInvite(req, res); }); router.post( - "/", + "/me", ParamaterPassCheckHelper.requiredIncludedMiddleware(["username", "mail", "firstname", "lastname"]), async (req, res) => { - await inviteUser(req, res, false); + await inviteUser(req, res, true); } ); -router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => { - await finishInvite(req, res, true); -}); +router.post( + "/finish", + ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), + async (req, res) => { + await finishInvite(req, res, true); + } +); export default router; diff --git a/src/routes/user.ts b/src/routes/user.ts index d196e16..68b6b7c 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -1,5 +1,16 @@ import express from "express"; -import { getMeById, getMyTotp, transferOwnership, updateMe, verifyMyTotp } from "../controller/userController"; +import { + changeMyPassword, + changeToPW, + changeToTOTP, + getChangeToTOTP, + getMeById, + getMyRoutine, + getMyTotp, + transferOwnership, + updateMe, + verifyMyTotp, +} from "../controller/userController"; var router = express.Router({ mergeParams: true }); @@ -7,14 +18,34 @@ router.get("/me", async (req, res) => { await getMeById(req, res); }); +router.get("/routine", async (req, res) => { + await getMyRoutine(req, res); +}); + router.get("/totp", async (req, res) => { await getMyTotp(req, res); }); +router.get("/changeToTOTP", async (req, res) => { + await getChangeToTOTP(req, res); +}); + router.post("/verify", async (req, res) => { await verifyMyTotp(req, res); }); +router.patch("/changepw", async (req, res) => { + await changeMyPassword(req, res); +}); + +router.patch("/changeToTOTP", async (req, res) => { + await changeToTOTP(req, res); +}); + +router.patch("/changeToPW", async (req, res) => { + await changeToPW(req, res); +}); + router.put("/transferOwner", async (req, res) => { await transferOwnership(req, res); }); diff --git a/src/service/club/calendarService.ts b/src/service/club/calendarService.ts index 7e8f86b..c6c7128 100644 --- a/src/service/club/calendarService.ts +++ b/src/service/club/calendarService.ts @@ -13,7 +13,7 @@ export default abstract class CalendarService { .getRepository(calendar) .createQueryBuilder("calendar") .leftJoinAndSelect("calendar.type", "type") - .orderBy("starttime", "ASC") + .orderBy("calendar.starttime", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/club/member/memberEducationService.ts b/src/service/club/member/memberEducationService.ts new file mode 100644 index 0000000..5b98032 --- /dev/null +++ b/src/service/club/member/memberEducationService.ts @@ -0,0 +1,49 @@ +import { dataSource } from "../../../data-source"; +import { memberEducations } from "../../../entity/club/member/memberEducations"; +import DatabaseActionException from "../../../exceptions/databaseActionException"; +import InternalException from "../../../exceptions/internalException"; + +export default abstract class MemberEducationService { + /** + * @description get all by member id + * @param {string} memberId + * @returns {Promise>} + */ + static async getAll(memberId: string): Promise> { + return await dataSource + .getRepository(memberEducations) + .createQueryBuilder("memberEducations") + .leftJoinAndSelect("memberEducations.education", "education") + .where("memberEducations.memberId = :memberId", { memberId: memberId }) + .orderBy("education.education", "ASC") + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "memberEducations", err); + }); + } + + /** + * @description get by memberId and recordId + * @param {string} memberId + * @param {number} recordId + * @returns {Promise>} + */ + static async getById(memberId: string, recordId: number): Promise { + return await dataSource + .getRepository(memberEducations) + .createQueryBuilder("memberEducations") + .leftJoinAndSelect("memberEducations.education", "education") + .where("memberEducations.memberId = :memberId", { memberId: memberId }) + .andWhere("memberEducations.id = :recordId", { recordId: recordId }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "memberEducations", err); + }); + } +} diff --git a/src/service/club/member/memberService.ts b/src/service/club/member/memberService.ts index 3eae4e6..0aed9aa 100644 --- a/src/service/club/member/memberService.ts +++ b/src/service/club/member/memberService.ts @@ -1,11 +1,8 @@ -import { Brackets, Like, SelectQueryBuilder } from "typeorm"; +import { Brackets, ILike, Not, SelectQueryBuilder } from "typeorm"; import { dataSource } from "../../../data-source"; import { member } from "../../../entity/club/member/member"; -import { membership } from "../../../entity/club/member/membership"; import DatabaseActionException from "../../../exceptions/databaseActionException"; -import InternalException from "../../../exceptions/internalException"; import { memberView } from "../../../views/memberView"; -import { DB_TYPE } from "../../../env.defaults"; export default abstract class MemberService { /** @@ -31,9 +28,14 @@ export default abstract class MemberService { let searchBits = search.split(" "); if (searchBits.length < 2) { - query = query.where(`member.firstname LIKE :searchQuery OR member.lastname LIKE :searchQuery`, { - searchQuery: `%${searchBits[0]}%`, - }); + query = query.where( + new Brackets((qb) => + qb + .where({ firstname: ILike(`%${searchBits[0]}%`) }) + .orWhere({ lastname: ILike(`%${searchBits[0]}%`) }) + .orWhere({ internalId: ILike(`%${searchBits[0]}%`) }) + ) + ); } else { searchBits .flatMap((v, i) => searchBits.slice(i + 1).map((w) => [v, w])) @@ -41,12 +43,12 @@ export default abstract class MemberService { query = query .orWhere( new Brackets((qb) => - qb.where({ firstname: Like(`%${term[0]}%`) }).andWhere({ lastname: Like(`%${term[1]}%`) }) + qb.where({ firstname: ILike(`%${term[0]}%`) }).andWhere({ lastname: ILike(`%${term[1]}%`) }) ) ) .orWhere( new Brackets((qb) => - qb.where({ firstname: Like(`%${term[1]}%`) }).andWhere({ lastname: Like(`%${term[0]}%`) }) + qb.where({ firstname: ILike(`%${term[1]}%`) }).andWhere({ lastname: ILike(`%${term[0]}%`) }) ) ); }); @@ -157,6 +159,28 @@ export default abstract class MemberService { }); } + /** + * @description get latest inserted memberId + * @returns {Promise} + */ + static async getLatestInternalId(): Promise { + return await dataSource + .getRepository(member) + .createQueryBuilder("member") + .where("member.internalId IS NOT NULL") + .andWhere({ internalId: Not("") }) + .orderBy("member.createdAt", "DESC") + .addOrderBy("member.internalId", "DESC") + .limit(1) + .getOne() + .then((res) => { + return res?.internalId ?? ""; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "memberId", err); + }); + } + /** * @description apply member joins to query * @returns {SelectQueryBuilder} @@ -169,17 +193,13 @@ export default abstract class MemberService { "member.firstMembershipEntry", "member.memberships", "membership_first", - DB_TYPE == "postgres" - ? 'membership_first.memberId = member.id AND membership_first.start = (SELECT MIN("m_first"."start") FROM "membership" "m_first" WHERE "m_first"."memberId" = "member"."id")' - : "membership_first.memberId = member.id AND membership_first.start = (SELECT MIN(m_first.start) FROM membership m_first WHERE m_first.memberId = member.id)" + 'membership_first.memberId = member.id AND membership_first.start = (SELECT MIN("m_first"."start") FROM "membership" "m_first" WHERE "m_first"."memberId" = "member"."id")' ) .leftJoinAndMapOne( "member.lastMembershipEntry", "member.memberships", "membership_last", - DB_TYPE == "postgres" - ? 'membership_last.memberId = member.id AND membership_last.start = (SELECT MAX("m_last"."start") FROM "membership" "m_last" WHERE "m_last"."memberId" = "member"."id")' - : "membership_last.memberId = member.id AND membership_last.start = (SELECT MAX(m_last.start) FROM membership m_last WHERE m_last.memberId = member.id)" + 'membership_last.memberId = member.id AND membership_last.start = (SELECT MAX("m_last"."start") FROM "membership" "m_last" WHERE "m_last"."memberId" = "member"."id")' ) .leftJoinAndSelect("membership_first.status", "status_first") .leftJoinAndSelect("membership_last.status", "status_last") diff --git a/src/service/club/member/membershipService.ts b/src/service/club/member/membershipService.ts index f5e7a07..d423b13 100644 --- a/src/service/club/member/membershipService.ts +++ b/src/service/club/member/membershipService.ts @@ -2,7 +2,7 @@ import { dataSource } from "../../../data-source"; import { membership } from "../../../entity/club/member/membership"; import DatabaseActionException from "../../../exceptions/databaseActionException"; import InternalException from "../../../exceptions/internalException"; -import { membershipView } from "../../../views/membershipsView"; +import { membershipTotalView, membershipView } from "../../../views/membershipsView"; export default abstract class MembershipService { /** @@ -66,4 +66,23 @@ export default abstract class MembershipService { throw new DatabaseActionException("SELECT", "membershipView", err); }); } + + /** + * @description get membership total statistics by memberId + * @param {string} memberId + * @returns {Promise>} + */ + static async getTotalStatisticsById(memberId: string): Promise { + return await dataSource + .getRepository(membershipTotalView) + .createQueryBuilder("membershipTotalView") + .where("membershipTotalView.memberId = :memberId", { memberId: memberId }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "membershipTotalView", err); + }); + } } diff --git a/src/service/club/newsletter/newsletterService.ts b/src/service/club/newsletter/newsletterService.ts index 27489d0..d2401b3 100644 --- a/src/service/club/newsletter/newsletterService.ts +++ b/src/service/club/newsletter/newsletterService.ts @@ -12,6 +12,7 @@ export default abstract class NewsletterService { return await dataSource .getRepository(newsletter) .createQueryBuilder("newsletter") + .orderBy("newsletter.createdAt", "DESC") .offset(offset) .limit(count) .getManyAndCount() diff --git a/src/service/club/protocol/protocolService.ts b/src/service/club/protocol/protocolService.ts index 185d76a..a8c5828 100644 --- a/src/service/club/protocol/protocolService.ts +++ b/src/service/club/protocol/protocolService.ts @@ -14,7 +14,7 @@ export default abstract class ProtocolService { .createQueryBuilder("protocol") .offset(offset) .limit(count) - .orderBy("date", "DESC") + .orderBy("protocol.date", "DESC") .getManyAndCount() .then((res) => { return res; diff --git a/src/service/configuration/awardService.ts b/src/service/configuration/awardService.ts index 223d676..f7a0b66 100644 --- a/src/service/configuration/awardService.ts +++ b/src/service/configuration/awardService.ts @@ -13,7 +13,7 @@ export default abstract class AwardService { return await dataSource .getRepository(award) .createQueryBuilder("award") - .orderBy("award", "ASC") + .orderBy("award.award", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/configuration/calendarTypeService.ts b/src/service/configuration/calendarTypeService.ts index 7d82ef4..a84e5a9 100644 --- a/src/service/configuration/calendarTypeService.ts +++ b/src/service/configuration/calendarTypeService.ts @@ -12,7 +12,7 @@ export default abstract class CalendarTypeService { return await dataSource .getRepository(calendarType) .createQueryBuilder("calendarType") - .orderBy("type", "ASC") + .orderBy("calendarType.type", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/configuration/communicationTypeService.ts b/src/service/configuration/communicationTypeService.ts index 4ee9de0..44fef7f 100644 --- a/src/service/configuration/communicationTypeService.ts +++ b/src/service/configuration/communicationTypeService.ts @@ -12,7 +12,7 @@ export default abstract class CommunicationTypeService { return await dataSource .getRepository(communicationType) .createQueryBuilder("communicationType") - .orderBy("type", "ASC") + .orderBy("communicationType.type", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/configuration/education.ts b/src/service/configuration/education.ts new file mode 100644 index 0000000..13a2e52 --- /dev/null +++ b/src/service/configuration/education.ts @@ -0,0 +1,41 @@ +import { dataSource } from "../../data-source"; +import { education } from "../../entity/configuration/education"; +import DatabaseActionException from "../../exceptions/databaseActionException"; + +export default abstract class EducationService { + /** + * @description get all educations + * @returns {Promise>} + */ + static async getAll(): Promise> { + return await dataSource + .getRepository(education) + .createQueryBuilder("education") + .orderBy("education.education", "ASC") + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "education", err); + }); + } + + /** + * @description get education by id + * @returns {Promise} + */ + static async getById(id: number): Promise { + return await dataSource + .getRepository(education) + .createQueryBuilder("education") + .where("education.id = :id", { id: id }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "education", err); + }); + } +} diff --git a/src/service/configuration/executivePositionService.ts b/src/service/configuration/executivePositionService.ts index 6a2bf8d..542093e 100644 --- a/src/service/configuration/executivePositionService.ts +++ b/src/service/configuration/executivePositionService.ts @@ -13,7 +13,7 @@ export default abstract class ExecutivePositionService { return await dataSource .getRepository(executivePosition) .createQueryBuilder("executivePosition") - .orderBy("position", "ASC") + .orderBy("executivePosition.position", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/configuration/membershipStatusService.ts b/src/service/configuration/membershipStatusService.ts index ade5a53..d8b5ded 100644 --- a/src/service/configuration/membershipStatusService.ts +++ b/src/service/configuration/membershipStatusService.ts @@ -13,7 +13,7 @@ export default abstract class MembershipStatusService { return await dataSource .getRepository(membershipStatus) .createQueryBuilder("membershipStatus") - .orderBy("status", "ASC") + .orderBy("membershipStatus.status", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/configuration/qualification.ts b/src/service/configuration/qualification.ts index 137bda2..367fe6a 100644 --- a/src/service/configuration/qualification.ts +++ b/src/service/configuration/qualification.ts @@ -14,7 +14,7 @@ export default abstract class QualificationService { return await dataSource .getRepository(qualification) .createQueryBuilder("qualification") - .orderBy("qualification", "ASC") + .orderBy("qualification.qualification", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/configuration/queryStoreService.ts b/src/service/configuration/queryStoreService.ts index ecce9cc..c2e969f 100644 --- a/src/service/configuration/queryStoreService.ts +++ b/src/service/configuration/queryStoreService.ts @@ -12,7 +12,7 @@ export default abstract class QueryStoreService { return await dataSource .getRepository(query) .createQueryBuilder("queryStore") - .orderBy("title", "ASC") + .orderBy("queryStore.title", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/configuration/salutationService.ts b/src/service/configuration/salutationService.ts index f8a020e..0e97cc1 100644 --- a/src/service/configuration/salutationService.ts +++ b/src/service/configuration/salutationService.ts @@ -12,7 +12,7 @@ export default abstract class SalutationService { return await dataSource .getRepository(salutation) .createQueryBuilder("salutation") - .orderBy("salutation", "ASC") + .orderBy("salutation.salutation", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/configuration/templateService.ts b/src/service/configuration/templateService.ts index dd3e008..fcc2eb6 100644 --- a/src/service/configuration/templateService.ts +++ b/src/service/configuration/templateService.ts @@ -13,7 +13,7 @@ export default abstract class TemplateService { return await dataSource .getRepository(template) .createQueryBuilder("template") - .orderBy("template", "ASC") + .orderBy("template.template", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/configuration/templateUsageService.ts b/src/service/configuration/templateUsageService.ts index ce6b508..99fe2dc 100644 --- a/src/service/configuration/templateUsageService.ts +++ b/src/service/configuration/templateUsageService.ts @@ -15,7 +15,7 @@ export default abstract class TemplateUsageService { .leftJoinAndSelect("templateUsage.header", "headerTemplate") .leftJoinAndSelect("templateUsage.body", "bodyTemplate") .leftJoinAndSelect("templateUsage.footer", "footerTemplate") - .orderBy("scope", "ASC") + .orderBy("templateUsage.scope", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/management/roleService.ts b/src/service/management/roleService.ts index c3a2dcd..4aa761b 100644 --- a/src/service/management/roleService.ts +++ b/src/service/management/roleService.ts @@ -13,7 +13,7 @@ export default abstract class RoleService { .getRepository(role) .createQueryBuilder("role") .leftJoinAndSelect("role.permissions", "role_permissions") - .orderBy("role", "ASC") + .orderBy("role.role", "ASC") .getMany() .then((res) => { return res; diff --git a/src/service/management/settingService.ts b/src/service/management/settingService.ts new file mode 100644 index 0000000..4f9b5e2 --- /dev/null +++ b/src/service/management/settingService.ts @@ -0,0 +1,43 @@ +import { dataSource } from "../../data-source"; +import { setting } from "../../entity/management/setting"; +import InternalException from "../../exceptions/internalException"; +import { SettingString } from "../../type/settingTypes"; + +export default abstract class SettingService { + /** + * @description get settings + * @returns {Promise} + */ + static async getSettings(): Promise { + return await dataSource + .getRepository(setting) + .createQueryBuilder("setting") + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("setting not found", err); + }); + } + + /** + * @description get setting + * @param token SettingString + * @returns {Promise} + */ + static async getBySettingString(key: SettingString): Promise { + return await dataSource + .getRepository(setting) + .createQueryBuilder("setting") + .where("setting.topic = :topic", { topic: key.split(".")[0] }) + .andWhere("setting.key >= :key", { key: key.split(".")[1] }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("setting not found", err); + }); + } +} diff --git a/src/service/management/userService.ts b/src/service/management/userService.ts index 8fe4dd4..dd877ee 100644 --- a/src/service/management/userService.ts +++ b/src/service/management/userService.ts @@ -16,8 +16,8 @@ export default abstract class UserService { .leftJoinAndSelect("user.roles", "roles") .leftJoinAndSelect("user.permissions", "permissions") .leftJoinAndSelect("roles.permissions", "role_permissions") - .orderBy("firstname", "ASC") - .addOrderBy("lastname", "ASC") + .orderBy("user.firstname", "ASC") + .addOrderBy("user.lastname", "ASC") .getMany() .then((res) => { return res; @@ -129,4 +129,27 @@ export default abstract class UserService { throw new DatabaseActionException("SELECT", "userRoles", err); }); } + + /** + * @description get secret and routine by iser + * @param userId string + * @returns {Promise} + */ + static async getUserSecretAndRoutine(userId: string): Promise { + return await dataSource + .getRepository(user) + .createQueryBuilder("user") + .select("user.id") + .addSelect("user.secret") + .addSelect("user.routine") + .where("user.id = :id", { id: userId }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + console.log(err); + throw new DatabaseActionException("SELECT", "user credentials", err); + }); + } } diff --git a/src/templates/member.body.template.html b/src/templates/member.body.template.html index 67f8821..6fd781c 100644 --- a/src/templates/member.body.template.html +++ b/src/templates/member.body.template.html @@ -58,7 +58,7 @@
{{/each}} {{/if}} {{#if qualifications.length}}
-

Qualifikationen

+

Qualifikationen / Funktionen

{{#each qualifications}}

{{this.qualification.qualification}}: {{date this.date}}

@@ -69,6 +69,19 @@ {{/if}}

+ {{/each}} {{/if}} {{#if educations.length}} +
+

Aus-/Fortbildungen

+ {{#each educations}} +
+

{{this.education.education}}: {{date this.start}}{{#if this.end}} bis {{date this.end}}{{/if}}

+ {{#if this.place}} +

Ausbildungsort: {{this.place}}

+ {{/if}}{{#if this.note}} +

Notiz: {{this.note}}

+ {{/if}} +
+
{{/each}} {{/if}}