Compare commits
13 commits
main
...
feature/#7
Author | SHA1 | Date | |
---|---|---|---|
964af82904 | |||
99eafcb352 | |||
ce9f621b8b | |||
2e3d0a755c | |||
70edd165ee | |||
b4a7986c8a | |||
7aa9038a61 | |||
a8edc19f34 | |||
730c25a9a1 | |||
f32143b7ac | |||
f850fc2526 | |||
f1395357c5 | |||
63f206cc6a |
51 changed files with 2003 additions and 166 deletions
19
.env.example
19
.env.example
|
@ -17,25 +17,10 @@ DB_PASSWORD = database_password
|
||||||
## BSP für sqlite
|
## BSP für sqlite
|
||||||
DB_HOST = filename.db
|
DB_HOST = filename.db
|
||||||
|
|
||||||
|
## Dev only
|
||||||
SERVER_PORT = portnumber
|
SERVER_PORT = portnumber
|
||||||
|
|
||||||
JWT_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 # besitzt default
|
APPLICATION_SECRET = mysecret
|
||||||
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
|
|
||||||
|
|
||||||
USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true
|
USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true
|
||||||
SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15m
|
SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15m
|
||||||
|
|
|
@ -37,6 +37,7 @@ RUN mkdir -p /app/files
|
||||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||||
|
|
||||||
COPY --from=build /app/src/templates /app/src/templates
|
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/dist /app/dist
|
||||||
COPY --from=build /app/node_modules /app/node_modules
|
COPY --from=build /app/node_modules /app/node_modules
|
||||||
COPY --from=build /app/package.json /app/package.json
|
COPY --from=build /app/package.json /app/package.json
|
||||||
|
|
586
package-lock.json
generated
586
package-lock.json
generated
|
@ -10,7 +10,9 @@
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
"email-check": "^1.1.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
"express-validator": "^7.2.1",
|
"express-validator": "^7.2.1",
|
||||||
|
@ -19,6 +21,7 @@
|
||||||
"ics": "^3.8.1",
|
"ics": "^3.8.1",
|
||||||
"ip": "^2.0.1",
|
"ip": "^2.0.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
|
@ -33,6 +36,8 @@
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rss-parser": "^3.13.0",
|
"rss-parser": "^3.13.0",
|
||||||
|
"sharp": "^0.34.1",
|
||||||
|
"sharp-ico": "^0.1.5",
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.7.5",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
|
@ -41,9 +46,11 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.14",
|
"@types/cors": "^2.8.14",
|
||||||
|
"@types/email-check": "^1.1.3",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
"@types/ip": "^1.1.3",
|
"@types/ip": "^1.1.3",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
"@types/lodash.clonedeep": "^4.5.9",
|
||||||
"@types/lodash.uniqby": "^4.7.9",
|
"@types/lodash.uniqby": "^4.7.9",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/ms": "^2.1.0",
|
"@types/ms": "^2.1.0",
|
||||||
|
@ -83,6 +90,12 @@
|
||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
|
@ -96,6 +109,23 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
||||||
|
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
||||||
|
"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": {
|
"node_modules/@gar/promisify": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
||||||
|
@ -103,6 +133,383 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
|
"version": "0.34.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==",
|
||||||
|
"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.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
|
"version": "0.34.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==",
|
||||||
|
"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.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==",
|
||||||
|
"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.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==",
|
||||||
|
"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.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==",
|
||||||
|
"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.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==",
|
||||||
|
"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.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==",
|
||||||
|
"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.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==",
|
||||||
|
"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.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==",
|
||||||
|
"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.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==",
|
||||||
|
"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.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==",
|
||||||
|
"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.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==",
|
||||||
|
"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.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
|
"version": "0.34.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==",
|
||||||
|
"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.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
|
"version": "0.34.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==",
|
||||||
|
"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.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
|
"version": "0.34.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==",
|
||||||
|
"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.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
|
"version": "0.34.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==",
|
||||||
|
"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.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
|
"version": "0.34.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==",
|
||||||
|
"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.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-wasm32": {
|
||||||
|
"version": "0.34.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/runtime": "^1.4.0"
|
||||||
|
},
|
||||||
|
"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.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==",
|
||||||
|
"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.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==",
|
||||||
|
"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": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
|
@ -401,6 +808,13 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/email-check": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/email-check/-/email-check-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-XgU2uxm8JjfK9e/CJg389b96XeLxJbUSCfe4hZxxwTu3XYT7A70punAWfpdppFHWPDl/qNtHC9vl3TmRHom+8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/express": {
|
"node_modules/@types/express": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz",
|
||||||
|
@ -461,6 +875,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/lodash.uniqby": {
|
||||||
"version": "4.7.9",
|
"version": "4.7.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash.uniqby/-/lodash.uniqby-4.7.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash.uniqby/-/lodash.uniqby-4.7.9.tgz",
|
||||||
|
@ -1348,6 +1772,19 @@
|
||||||
"node": ">=12"
|
"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": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
@ -1366,6 +1803,16 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/color-support": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||||
|
@ -1522,6 +1969,13 @@
|
||||||
"node": ">= 8"
|
"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": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
||||||
|
@ -1563,6 +2017,33 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/decompress-response": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
@ -1695,6 +2176,18 @@
|
||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/email-check": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/email-check/-/email-check-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-VoqdsHtP/Ct+Dsl9nJRlvVXhcHicWjmmp2KvLbyg+WovdUXihe8EbDKC5u+3SlBQIlh8RK1qFD5A4RCgTrW9Wg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-promisify": "1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
@ -2472,6 +2965,12 @@
|
||||||
"ms": "^2.0.0"
|
"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": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
@ -2665,6 +3164,15 @@
|
||||||
"@pkgjs/parseargs": "^0.11.0"
|
"@pkgjs/parseargs": "^0.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-promisify": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-promisify/-/js-promisify-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-/IBrGxYbrmRWA+rLtHVSiX7R92NuVqc84aSWXReEjwcj7NchYf+Wy/ShAapCmMM5ev0mvD2IhWmZIDk/7f/utQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
@ -2762,6 +3270,12 @@
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
@ -4449,6 +4963,57 @@
|
||||||
"sha.js": "bin.js"
|
"sha.js": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sharp": {
|
||||||
|
"version": "0.34.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz",
|
||||||
|
"integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"color": "^4.2.3",
|
||||||
|
"detect-libc": "^2.0.3",
|
||||||
|
"semver": "^7.7.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-darwin-arm64": "0.34.1",
|
||||||
|
"@img/sharp-darwin-x64": "0.34.1",
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linux-ppc64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.1.0",
|
||||||
|
"@img/sharp-linux-arm": "0.34.1",
|
||||||
|
"@img/sharp-linux-arm64": "0.34.1",
|
||||||
|
"@img/sharp-linux-s390x": "0.34.1",
|
||||||
|
"@img/sharp-linux-x64": "0.34.1",
|
||||||
|
"@img/sharp-linuxmusl-arm64": "0.34.1",
|
||||||
|
"@img/sharp-linuxmusl-x64": "0.34.1",
|
||||||
|
"@img/sharp-wasm32": "0.34.1",
|
||||||
|
"@img/sharp-win32-ia32": "0.34.1",
|
||||||
|
"@img/sharp-win32-x64": "0.34.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
@ -4594,6 +5159,21 @@
|
||||||
"simple-concat": "^1.0.0"
|
"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": {
|
"node_modules/smart-buffer": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
@ -5078,6 +5658,12 @@
|
||||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"start_ts": "ts-node src/index.ts",
|
"start_ts": "ts-node src/index.ts",
|
||||||
"typeorm": "typeorm-ts-node-commonjs",
|
"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": "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",
|
"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",
|
"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",
|
"revert-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:revert -d ./src/data-source.ts",
|
||||||
|
@ -25,7 +26,9 @@
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
"email-check": "^1.1.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
"express-validator": "^7.2.1",
|
"express-validator": "^7.2.1",
|
||||||
|
@ -34,6 +37,7 @@
|
||||||
"ics": "^3.8.1",
|
"ics": "^3.8.1",
|
||||||
"ip": "^2.0.1",
|
"ip": "^2.0.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
|
@ -48,6 +52,8 @@
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rss-parser": "^3.13.0",
|
"rss-parser": "^3.13.0",
|
||||||
|
"sharp": "^0.34.1",
|
||||||
|
"sharp-ico": "^0.1.5",
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.7.5",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
|
@ -56,9 +62,11 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.14",
|
"@types/cors": "^2.8.14",
|
||||||
|
"@types/email-check": "^1.1.3",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
"@types/ip": "^1.1.3",
|
"@types/ip": "^1.1.3",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
"@types/lodash.clonedeep": "^4.5.9",
|
||||||
"@types/lodash.uniqby": "^4.7.9",
|
"@types/lodash.uniqby": "^4.7.9",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/ms": "^2.1.0",
|
"@types/ms": "^2.1.0",
|
||||||
|
|
BIN
src/assets/admin-logo.png
Normal file
BIN
src/assets/admin-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/icon.png
Normal file
BIN
src/assets/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -1,8 +1,8 @@
|
||||||
import { NewsletterConfigType } from "../../../enums/newsletterConfigType";
|
import { NewsletterConfigEnum } from "../../../enums/newsletterConfigEnum";
|
||||||
|
|
||||||
export interface SetNewsletterConfigCommand {
|
export interface SetNewsletterConfigCommand {
|
||||||
comTypeId: number;
|
comTypeId: number;
|
||||||
config: NewsletterConfigType;
|
config: NewsletterConfigEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteNewsletterConfigCommand {
|
export interface DeleteNewsletterConfigCommand {
|
||||||
|
|
10
src/command/management/setting/settingCommand.ts
Normal file
10
src/command/management/setting/settingCommand.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export interface CreateOrUpdateSettingCommand {
|
||||||
|
topic: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteSettingCommand {
|
||||||
|
topic: string;
|
||||||
|
key: string;
|
||||||
|
}
|
53
src/command/management/setting/settingCommandHandler.ts
Normal file
53
src/command/management/setting/settingCommandHandler.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { dataSource } from "../../../data-source";
|
||||||
|
import { setting } from "../../../entity/management/setting";
|
||||||
|
import DatabaseActionException from "../../../exceptions/databaseActionException";
|
||||||
|
import { StringHelper } from "../../../helpers/stringHelper";
|
||||||
|
import { CreateOrUpdateSettingCommand, DeleteSettingCommand } from "./settingCommand";
|
||||||
|
|
||||||
|
export default abstract class SettingCommandHandler {
|
||||||
|
/**
|
||||||
|
* @description create setting
|
||||||
|
* @param {CreateOrUpdateSettingCommand} createSetting
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
static async create(createSetting: CreateOrUpdateSettingCommand): Promise<string> {
|
||||||
|
const token = StringHelper.random(32);
|
||||||
|
|
||||||
|
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 token;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw new DatabaseActionException("CREATE OR UPDATE", "setting", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description delete setting by topic and key
|
||||||
|
* @param {DeleteRefreshCommand} deleteSetting
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
static async delete(deleteSetting: DeleteSettingCommand): Promise<any> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,8 @@
|
||||||
import { dataSource } from "../data-source";
|
import { dataSource } from "../data-source";
|
||||||
import { refresh } from "../entity/refresh";
|
import { refresh } from "../entity/refresh";
|
||||||
import { PWA_REFRESH_EXPIRATION, REFRESH_EXPIRATION } from "../env.defaults";
|
|
||||||
import DatabaseActionException from "../exceptions/databaseActionException";
|
import DatabaseActionException from "../exceptions/databaseActionException";
|
||||||
import InternalException from "../exceptions/internalException";
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
import { StringHelper } from "../helpers/stringHelper";
|
import { StringHelper } from "../helpers/stringHelper";
|
||||||
import UserService from "../service/management/userService";
|
|
||||||
import { CreateRefreshCommand, DeleteRefreshCommand } from "./refreshCommand";
|
import { CreateRefreshCommand, DeleteRefreshCommand } from "./refreshCommand";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
|
@ -25,8 +23,8 @@ export default abstract class RefreshCommandHandler {
|
||||||
token: refreshToken,
|
token: refreshToken,
|
||||||
userId: createRefresh.userId,
|
userId: createRefresh.userId,
|
||||||
expiry: createRefresh.isFromPwa
|
expiry: createRefresh.isFromPwa
|
||||||
? new Date(Date.now() + ms(PWA_REFRESH_EXPIRATION))
|
? new Date(Date.now() + ms(SettingHelper.getSetting("session.pwa_refresh_expiration")))
|
||||||
: new Date(Date.now() + ms(REFRESH_EXPIRATION)),
|
: new Date(Date.now() + ms(SettingHelper.getSetting("session.refresh_expiration"))),
|
||||||
})
|
})
|
||||||
.execute()
|
.execute()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
|
60
src/controller/admin/management/settingController.ts
Normal file
60
src/controller/admin/management/settingController.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import SettingHelper from "../../../helpers/settingsHelper";
|
||||||
|
import { SettingString } from "../../../type/settingTypes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
let setting = req.body.setting as SettingString;
|
||||||
|
let value = req.body.value as string;
|
||||||
|
|
||||||
|
SettingHelper.setSetting(setting, value);
|
||||||
|
|
||||||
|
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<any> {
|
||||||
|
let setting = req.params.setting as SettingString;
|
||||||
|
|
||||||
|
SettingHelper.resetSetting(setting);
|
||||||
|
|
||||||
|
res.sendStatus(204);
|
||||||
|
}
|
|
@ -11,10 +11,10 @@ import {
|
||||||
} from "../../../command/management/user/userCommand";
|
} from "../../../command/management/user/userCommand";
|
||||||
import UserCommandHandler from "../../../command/management/user/userCommandHandler";
|
import UserCommandHandler from "../../../command/management/user/userCommandHandler";
|
||||||
import MailHelper from "../../../helpers/mailHelper";
|
import MailHelper from "../../../helpers/mailHelper";
|
||||||
import { CLUB_NAME } from "../../../env.defaults";
|
|
||||||
import { UpdateUserPermissionsCommand } from "../../../command/management/user/userPermissionCommand";
|
import { UpdateUserPermissionsCommand } from "../../../command/management/user/userPermissionCommand";
|
||||||
import UserPermissionCommandHandler from "../../../command/management/user/userPermissionCommandHandler";
|
import UserPermissionCommandHandler from "../../../command/management/user/userPermissionCommandHandler";
|
||||||
import BadRequestException from "../../../exceptions/badRequestException";
|
import BadRequestException from "../../../exceptions/badRequestException";
|
||||||
|
import SettingHelper from "../../../helpers/settingsHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get All users
|
* @description get All users
|
||||||
|
@ -157,7 +157,7 @@ export async function deleteUser(req: Request, res: Response): Promise<any> {
|
||||||
// sendmail
|
// sendmail
|
||||||
await MailHelper.sendMail(
|
await MailHelper.sendMail(
|
||||||
mail,
|
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.`
|
`Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.`
|
||||||
);
|
);
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
|
@ -12,8 +12,8 @@ import WebapiCommandHandler from "../../../command/management/webapi/webapiComma
|
||||||
import { UpdateWebapiPermissionsCommand } from "../../../command/management/webapi/webapiPermissionCommand";
|
import { UpdateWebapiPermissionsCommand } from "../../../command/management/webapi/webapiPermissionCommand";
|
||||||
import WebapiPermissionCommandHandler from "../../../command/management/webapi/webapiPermissionCommandHandler";
|
import WebapiPermissionCommandHandler from "../../../command/management/webapi/webapiPermissionCommandHandler";
|
||||||
import { JWTHelper } from "../../../helpers/jwtHelper";
|
import { JWTHelper } from "../../../helpers/jwtHelper";
|
||||||
import { CLUB_NAME } from "../../../env.defaults";
|
|
||||||
import { StringHelper } from "../../../helpers/stringHelper";
|
import { StringHelper } from "../../../helpers/stringHelper";
|
||||||
|
import SettingHelper from "../../../helpers/settingsHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get All apis
|
* @description get All apis
|
||||||
|
@ -78,7 +78,7 @@ export async function createWebapi(req: Request, res: Response): Promise<any> {
|
||||||
|
|
||||||
let token = await JWTHelper.create(
|
let token = await JWTHelper.create(
|
||||||
{
|
{
|
||||||
iss: CLUB_NAME,
|
iss: SettingHelper.getSetting("club.name"),
|
||||||
sub: "api_token_retrieve",
|
sub: "api_token_retrieve",
|
||||||
aud: StringHelper.random(32),
|
aud: StringHelper.random(32),
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { JWTHelper } from "../helpers/jwtHelper";
|
import { JWTHelper } from "../helpers/jwtHelper";
|
||||||
import { JWTToken } from "../type/jwtTypes";
|
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import RefreshCommandHandler from "../command/refreshCommandHandler";
|
import RefreshCommandHandler from "../command/refreshCommandHandler";
|
||||||
import { CreateRefreshCommand } from "../command/refreshCommand";
|
import { CreateRefreshCommand } from "../command/refreshCommand";
|
||||||
|
@ -15,10 +14,8 @@ import MailHelper from "../helpers/mailHelper";
|
||||||
import InviteService from "../service/management/inviteService";
|
import InviteService from "../service/management/inviteService";
|
||||||
import UserService from "../service/management/userService";
|
import UserService from "../service/management/userService";
|
||||||
import CustomRequestException from "../exceptions/customRequestException";
|
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 InviteFactory from "../factory/admin/management/invite";
|
||||||
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get all invites
|
* @description get all invites
|
||||||
|
@ -38,7 +35,7 @@ export async function getInvites(req: Request, res: Response): Promise<any> {
|
||||||
* @param res {Response} Express res object
|
* @param res {Response} Express res object
|
||||||
* @returns {Promise<*>}
|
* @returns {Promise<*>}
|
||||||
*/
|
*/
|
||||||
export async function inviteUser(req: Request, res: Response, isInvite: boolean = true): Promise<any> {
|
export async function inviteUser(req: Request, res: Response, isSetup: boolean = false): Promise<any> {
|
||||||
let origin = req.headers.origin;
|
let origin = req.headers.origin;
|
||||||
let username = req.body.username;
|
let username = req.body.username;
|
||||||
let mail = req.body.mail;
|
let mail = req.body.mail;
|
||||||
|
@ -59,7 +56,7 @@ export async function inviteUser(req: Request, res: Response, isInvite: boolean
|
||||||
throw new CustomRequestException(409, "Username and Mail are already in use");
|
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 = {
|
let createInvite: CreateInviteCommand = {
|
||||||
username: username,
|
username: username,
|
||||||
|
@ -73,8 +70,8 @@ export async function inviteUser(req: Request, res: Response, isInvite: boolean
|
||||||
// sendmail
|
// sendmail
|
||||||
await MailHelper.sendMail(
|
await MailHelper.sendMail(
|
||||||
mail,
|
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}/${isInvite ? "invite" : "setup"}/verify?mail=${mail}&token=${token}`
|
`Öffne folgenden Link: ${origin}/${isSetup ? "setup" : "invite"}/verify?mail=${mail}&token=${token}`
|
||||||
);
|
);
|
||||||
|
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
|
@ -92,7 +89,7 @@ export async function verifyInvite(req: Request, res: Response): Promise<any> {
|
||||||
|
|
||||||
let { secret, username } = await InviteService.getByMailAndToken(mail, token);
|
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)
|
QRCode.toDataURL(url)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
|
|
@ -2,11 +2,13 @@ import { Request, Response } from "express";
|
||||||
import CalendarService from "../service/club/calendarService";
|
import CalendarService from "../service/club/calendarService";
|
||||||
import CalendarTypeService from "../service/configuration/calendarTypeService";
|
import CalendarTypeService from "../service/configuration/calendarTypeService";
|
||||||
import { calendar } from "../entity/club/calendar";
|
import { calendar } from "../entity/club/calendar";
|
||||||
import { createEvents } from "ics";
|
|
||||||
import moment from "moment";
|
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import CalendarFactory from "../factory/admin/club/calendar";
|
import CalendarFactory from "../factory/admin/club/calendar";
|
||||||
import { CalendarHelper } from "../helpers/calendarHelper";
|
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
|
* @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);
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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 == "") {
|
||||||
|
res.sendFile(FileSystemHelper.readAssetFile("admin-logo.png", true));
|
||||||
|
} else {
|
||||||
|
res.sendFile(FileSystemHelper.formatPath("/app/admin-logo.png"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<any> {
|
||||||
|
let icon = FileSystemHelper.readAssetFile("icon.png", true);
|
||||||
|
let setLogo = SettingHelper.getSetting("club.icon");
|
||||||
|
|
||||||
|
if (setLogo != "") {
|
||||||
|
icon = FileSystemHelper.formatPath("/app/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<any> {
|
||||||
|
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 != "") {
|
||||||
|
icon = FileSystemHelper.formatPath("/app/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);
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { JWTHelper } from "../helpers/jwtHelper";
|
import { JWTHelper } from "../helpers/jwtHelper";
|
||||||
import { JWTToken } from "../type/jwtTypes";
|
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import RefreshCommandHandler from "../command/refreshCommandHandler";
|
import RefreshCommandHandler from "../command/refreshCommandHandler";
|
||||||
import { CreateRefreshCommand } from "../command/refreshCommand";
|
import { CreateRefreshCommand } from "../command/refreshCommand";
|
||||||
|
@ -12,12 +11,9 @@ import ResetCommandHandler from "../command/resetCommandHandler";
|
||||||
import MailHelper from "../helpers/mailHelper";
|
import MailHelper from "../helpers/mailHelper";
|
||||||
import ResetService from "../service/resetService";
|
import ResetService from "../service/resetService";
|
||||||
import UserService from "../service/management/userService";
|
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 { UpdateUserSecretCommand } from "../command/management/user/userCommand";
|
||||||
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
||||||
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description request totp reset
|
* @description request totp reset
|
||||||
|
@ -31,7 +27,7 @@ export async function startReset(req: Request, res: Response): Promise<any> {
|
||||||
|
|
||||||
let { mail } = await UserService.getByUsername(username);
|
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 = {
|
let createReset: CreateResetCommand = {
|
||||||
username: username,
|
username: username,
|
||||||
|
@ -43,7 +39,7 @@ export async function startReset(req: Request, res: Response): Promise<any> {
|
||||||
// sendmail
|
// sendmail
|
||||||
await MailHelper.sendMail(
|
await MailHelper.sendMail(
|
||||||
mail,
|
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}`
|
`Öffne folgenden Link: ${origin}/reset/reset?mail=${mail}&token=${token}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -62,7 +58,7 @@ export async function verifyReset(req: Request, res: Response): Promise<any> {
|
||||||
|
|
||||||
let { secret } = await ResetService.getByMailAndToken(mail, token);
|
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)
|
QRCode.toDataURL(url)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import { Request, Response } from "express";
|
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
|
* @description Service is currently not configured
|
||||||
|
@ -9,3 +12,131 @@ import { Request, Response } from "express";
|
||||||
export async function isSetup(req: Request, res: Response): Promise<any> {
|
export async function isSetup(req: Request, res: Response): Promise<any> {
|
||||||
res.sendStatus(204);
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ import { Request, Response } from "express";
|
||||||
import speakeasy from "speakeasy";
|
import speakeasy from "speakeasy";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import { CLUB_NAME } from "../env.defaults";
|
|
||||||
import UserService from "../service/management/userService";
|
import UserService from "../service/management/userService";
|
||||||
import UserFactory from "../factory/admin/management/user";
|
import UserFactory from "../factory/admin/management/user";
|
||||||
import { TransferUserOwnerCommand, UpdateUserCommand } from "../command/management/user/userCommand";
|
import { TransferUserOwnerCommand, UpdateUserCommand } from "../command/management/user/userCommand";
|
||||||
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
||||||
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
||||||
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get my by id
|
* @description get my by id
|
||||||
|
@ -33,7 +33,7 @@ export async function getMyTotp(req: Request, res: Response): Promise<any> {
|
||||||
|
|
||||||
let { secret } = await UserService.getById(userId);
|
let { secret } = await UserService.getById(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)
|
QRCode.toDataURL(url)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import { DataSource } from "typeorm";
|
import { DataSource } from "typeorm";
|
||||||
import { DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME, DB_TYPE, DB_PORT } from "./env.defaults";
|
|
||||||
|
|
||||||
import { user } from "./entity/management/user";
|
import { user } from "./entity/management/user";
|
||||||
import { refresh } from "./entity/refresh";
|
import { refresh } from "./entity/refresh";
|
||||||
|
@ -51,6 +50,9 @@ import { TemplatesAndProtocolSort1742549956787 } from "./migrations/174254995678
|
||||||
import { QueryToUUID1742922178643 } from "./migrations/1742922178643-queryToUUID";
|
import { QueryToUUID1742922178643 } from "./migrations/1742922178643-queryToUUID";
|
||||||
import { NewsletterColumnType1744351418751 } from "./migrations/1744351418751-newsletterColumnType";
|
import { NewsletterColumnType1744351418751 } from "./migrations/1744351418751-newsletterColumnType";
|
||||||
import { QueryUpdatedAt1744795756230 } from "./migrations/1744795756230-QueryUpdatedAt";
|
import { QueryUpdatedAt1744795756230 } from "./migrations/1744795756230-QueryUpdatedAt";
|
||||||
|
import { setting } from "./entity/management/setting";
|
||||||
|
import { SettingsFromEnv1745059495808 } from "./migrations/1745059495808-settingsFromEnv";
|
||||||
|
import { DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_TYPE, DB_USERNAME } from "./env.defaults";
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
const dataSource = new DataSource({
|
||||||
type: DB_TYPE as any,
|
type: DB_TYPE as any,
|
||||||
|
@ -103,6 +105,7 @@ const dataSource = new DataSource({
|
||||||
membershipView,
|
membershipView,
|
||||||
webapi,
|
webapi,
|
||||||
webapiPermission,
|
webapiPermission,
|
||||||
|
setting,
|
||||||
],
|
],
|
||||||
migrations: [
|
migrations: [
|
||||||
BackupAndResetDatabase1738166124200,
|
BackupAndResetDatabase1738166124200,
|
||||||
|
@ -111,6 +114,7 @@ const dataSource = new DataSource({
|
||||||
QueryToUUID1742922178643,
|
QueryToUUID1742922178643,
|
||||||
NewsletterColumnType1744351418751,
|
NewsletterColumnType1744351418751,
|
||||||
QueryUpdatedAt1744795756230,
|
QueryUpdatedAt1744795756230,
|
||||||
|
SettingsFromEnv1745059495808,
|
||||||
],
|
],
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
migrationsTransactionMode: "each",
|
migrationsTransactionMode: "each",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
|
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
|
||||||
import { NewsletterConfigType } from "../../enums/newsletterConfigType";
|
import { NewsletterConfigEnum } from "../../enums/newsletterConfigEnum";
|
||||||
import { communicationType } from "./communicationType";
|
import { communicationType } from "./communicationType";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@ -11,15 +11,15 @@ export class newsletterConfig {
|
||||||
type: "varchar",
|
type: "varchar",
|
||||||
length: "255",
|
length: "255",
|
||||||
transformer: {
|
transformer: {
|
||||||
to(value: NewsletterConfigType) {
|
to(value: NewsletterConfigEnum) {
|
||||||
return value.toString();
|
return value.toString();
|
||||||
},
|
},
|
||||||
from(value: string) {
|
from(value: string) {
|
||||||
return NewsletterConfigType[value as keyof typeof NewsletterConfigType];
|
return NewsletterConfigEnum[value as keyof typeof NewsletterConfigEnum];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
config: NewsletterConfigType;
|
config: NewsletterConfigEnum;
|
||||||
|
|
||||||
@ManyToOne(() => communicationType, {
|
@ManyToOne(() => communicationType, {
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
|
13
src/entity/management/setting.ts
Normal file
13
src/entity/management/setting.ts
Normal file
|
@ -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;
|
||||||
|
}
|
5
src/enums/newsletterConfigEnum.ts
Normal file
5
src/enums/newsletterConfigEnum.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export enum NewsletterConfigEnum {
|
||||||
|
pdf = "pdf",
|
||||||
|
mail = "mail",
|
||||||
|
none = "none",
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
export enum NewsletterConfigType {
|
|
||||||
pdf = "pdf",
|
|
||||||
mail = "mail",
|
|
||||||
}
|
|
|
@ -11,23 +11,7 @@ export const DB_PASSWORD = process.env.DB_PASSWORD ?? "";
|
||||||
|
|
||||||
export const SERVER_PORT = Number(process.env.SERVER_PORT ?? 5000);
|
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 APPLICATION_SECRET = process.env.APPLICATION_SECRET;
|
||||||
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 USE_SECURITY_STRICT_LIMIT = process.env.USE_SECURITY_STRICT_LIMIT ?? "true";
|
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;
|
export const SECURITY_STRICT_LIMIT_WINDOW = (process.env.SECURITY_STRICT_LIMIT_WINDOW ?? "15m") as ms.StringValue;
|
||||||
|
@ -67,28 +51,6 @@ export function configCheck() {
|
||||||
|
|
||||||
if (isNaN(SERVER_PORT)) throw new Error("set valid numeric value to SERVER_PORT");
|
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")
|
if (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false")
|
||||||
throw new Error("set 'true' or 'false' to USE_SECURITY_STRICT_LIMIT");
|
throw new Error("set 'true' or 'false' to USE_SECURITY_STRICT_LIMIT");
|
||||||
checkMS(SECURITY_STRICT_LIMIT_WINDOW, "SECURITY_STRICT_LIMIT_WINDOW");
|
checkMS(SECURITY_STRICT_LIMIT_WINDOW, "SECURITY_STRICT_LIMIT_WINDOW");
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { EntityManager } from "typeorm";
|
||||||
import uniqBy from "lodash.uniqby";
|
import uniqBy from "lodash.uniqby";
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import UserService from "../service/management/userService";
|
import UserService from "../service/management/userService";
|
||||||
import { BACKUP_COPIES, BACKUP_INTERVAL } from "../env.defaults";
|
|
||||||
import DatabaseActionException from "../exceptions/databaseActionException";
|
import DatabaseActionException from "../exceptions/databaseActionException";
|
||||||
import { availableTemplates } from "../type/templateTypes";
|
import { availableTemplates } from "../type/templateTypes";
|
||||||
|
import SettingHelper from "./settingsHelper";
|
||||||
|
|
||||||
export type BackupSection =
|
export type BackupSection =
|
||||||
| "member"
|
| "member"
|
||||||
|
@ -18,7 +18,8 @@ export type BackupSection =
|
||||||
| "query"
|
| "query"
|
||||||
| "template"
|
| "template"
|
||||||
| "user"
|
| "user"
|
||||||
| "webapi";
|
| "webapi"
|
||||||
|
| "settings";
|
||||||
|
|
||||||
export type BackupSectionRefered = {
|
export type BackupSectionRefered = {
|
||||||
[key in BackupSection]?: Array<string>;
|
[key in BackupSection]?: Array<string>;
|
||||||
|
@ -42,6 +43,7 @@ export default abstract class BackupHelper {
|
||||||
{ type: "template", orderOnInsert: 2, orderOnClear: 1 }, // INSERT depends on member com
|
{ type: "template", orderOnInsert: 2, orderOnClear: 1 }, // INSERT depends on member com
|
||||||
{ type: "user", orderOnInsert: 1, orderOnClear: 1 },
|
{ type: "user", orderOnInsert: 1, orderOnClear: 1 },
|
||||||
{ type: "webapi", orderOnInsert: 1, orderOnClear: 1 },
|
{ type: "webapi", orderOnInsert: 1, orderOnClear: 1 },
|
||||||
|
{ type: "settings", orderOnInsert: 1, orderOnClear: 1 },
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly backupSectionRefered: BackupSectionRefered = {
|
private static readonly backupSectionRefered: BackupSectionRefered = {
|
||||||
|
@ -76,6 +78,7 @@ export default abstract class BackupHelper {
|
||||||
template: ["template", "template_usage"],
|
template: ["template", "template_usage"],
|
||||||
user: ["user", "user_permission", "role", "role_permission", "invite"],
|
user: ["user", "user_permission", "role", "role_permission", "invite"],
|
||||||
webapi: ["webapi", "webapi_permission"],
|
webapi: ["webapi", "webapi_permission"],
|
||||||
|
settings: ["setting"],
|
||||||
};
|
};
|
||||||
|
|
||||||
private static transactionManager: EntityManager;
|
private static transactionManager: EntityManager;
|
||||||
|
@ -103,7 +106,7 @@ export default abstract class BackupHelper {
|
||||||
let files = FileSystemHelper.getFilesInDirectory("backup", ".json");
|
let files = FileSystemHelper.getFilesInDirectory("backup", ".json");
|
||||||
let sorted = files.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime());
|
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) {
|
for (const file of filesToDelete) {
|
||||||
FileSystemHelper.deleteFile("backup", file);
|
FileSystemHelper.deleteFile("backup", file);
|
||||||
}
|
}
|
||||||
|
@ -117,7 +120,7 @@ export default abstract class BackupHelper {
|
||||||
let diffInMs = new Date().getTime() - lastBackup.getTime();
|
let diffInMs = new Date().getTime() - lastBackup.getTime();
|
||||||
let diffInDays = diffInMs / (1000 * 60 * 60 * 24);
|
let diffInDays = diffInMs / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
if (diffInDays >= BACKUP_INTERVAL) {
|
if (diffInDays >= SettingHelper.getSetting("backup.interval")) {
|
||||||
await this.createBackup({});
|
await this.createBackup({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,6 +223,8 @@ export default abstract class BackupHelper {
|
||||||
return await this.getUser(collectIds);
|
return await this.getUser(collectIds);
|
||||||
case "webapi":
|
case "webapi":
|
||||||
return await this.getWebapi();
|
return await this.getWebapi();
|
||||||
|
case "settings":
|
||||||
|
return await this.getSettings();
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -460,6 +465,13 @@ export default abstract class BackupHelper {
|
||||||
.addSelect(["permissions.permission"])
|
.addSelect(["permissions.permission"])
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
private static async getSettings(): Promise<Array<any>> {
|
||||||
|
return await dataSource
|
||||||
|
.getRepository("setting")
|
||||||
|
.createQueryBuilder("setting")
|
||||||
|
.select(["setting.topic", "setting.key", "setting.value"])
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
private static async setSectionData(
|
private static async setSectionData(
|
||||||
section: BackupSection,
|
section: BackupSection,
|
||||||
|
@ -476,6 +488,7 @@ export default abstract class BackupHelper {
|
||||||
if (section == "template" && !Array.isArray(data)) await this.setTemplate(data);
|
if (section == "template" && !Array.isArray(data)) await this.setTemplate(data);
|
||||||
if (section == "user" && !Array.isArray(data)) await this.setUser(data);
|
if (section == "user" && !Array.isArray(data)) await this.setUser(data);
|
||||||
if (section == "webapi" && Array.isArray(data)) await this.setWebapi(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<any>): Promise<void> {
|
private static async setMemberData(data: Array<any>): Promise<void> {
|
||||||
|
@ -810,4 +823,7 @@ export default abstract class BackupHelper {
|
||||||
private static async setWebapi(data: Array<any>): Promise<void> {
|
private static async setWebapi(data: Array<any>): Promise<void> {
|
||||||
await this.transactionManager.getRepository("webapi").save(data);
|
await this.transactionManager.getRepository("webapi").save(data);
|
||||||
}
|
}
|
||||||
|
private static async setSettings(data: Array<any>): Promise<void> {
|
||||||
|
await this.transactionManager.getRepository("setting").save(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createEvents } from "ics";
|
import { createEvents } from "ics";
|
||||||
import { calendar } from "../entity/club/calendar";
|
import { calendar } from "../entity/club/calendar";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { CLUB_NAME, CLUB_WEBSITE, MAIL_USERNAME } from "../env.defaults";
|
import SettingHelper from "./settingsHelper";
|
||||||
|
|
||||||
export abstract class CalendarHelper {
|
export abstract class CalendarHelper {
|
||||||
public static buildICS(entries: Array<calendar>): { error?: Error; value?: string } {
|
public static buildICS(entries: Array<calendar>): { error?: Error; value?: string } {
|
||||||
|
@ -35,7 +35,10 @@ export abstract class CalendarHelper {
|
||||||
description: i.content,
|
description: i.content,
|
||||||
location: i.location,
|
location: i.location,
|
||||||
categories: [i.type.type],
|
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)
|
created: moment(i.createdAt)
|
||||||
.format("YYYY-M-D-H-m")
|
.format("YYYY-M-D-H-m")
|
||||||
.split("-")
|
.split("-")
|
||||||
|
@ -46,7 +49,7 @@ export abstract class CalendarHelper {
|
||||||
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
||||||
transp: "OPAQUE" as "OPAQUE",
|
transp: "OPAQUE" as "OPAQUE",
|
||||||
status: "CONFIRMED",
|
status: "CONFIRMED",
|
||||||
...(CLUB_WEBSITE != "" ? { url: CLUB_WEBSITE } : {}),
|
...(SettingHelper.getSetting("club.website") != "" ? { url: SettingHelper.getSetting("club.website") } : {}),
|
||||||
alarms: [
|
alarms: [
|
||||||
{
|
{
|
||||||
action: "display",
|
action: "display",
|
||||||
|
|
86
src/helpers/codingHelper.ts
Normal file
86
src/helpers/codingHelper.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
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) return fallback;
|
||||||
|
try {
|
||||||
|
return CodingHelper.decrypt(key, val) || fallback;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Decryption error:", error);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
to(val: string | null | undefined): string {
|
||||||
|
const valueToEncrypt = val || fallback;
|
||||||
|
if (valueToEncrypt === "") return "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
return CodingHelper.encrypt(key, valueToEncrypt);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Encryption error:", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static encrypt(phrase: string, content: string): string {
|
||||||
|
if (!content) return "";
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decrypt(phrase: string, content: string): 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) {
|
||||||
|
console.error("Decryption failed:", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
105
src/helpers/convertHelper.ts
Normal file
105
src/helpers/convertHelper.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
export abstract class TypeConverter<T> {
|
||||||
|
abstract fromString(value: string): T;
|
||||||
|
abstract toString(value: T): string;
|
||||||
|
abstract validate(value: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class StringTypeConverter extends TypeConverter<string> {
|
||||||
|
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<number> {
|
||||||
|
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<boolean> {
|
||||||
|
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<ms.StringValue> {
|
||||||
|
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<string> {
|
||||||
|
fromString(value: string): string {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
toString(value: string): string {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
validate(value: string): boolean {
|
||||||
|
var tester =
|
||||||
|
/^[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/;
|
||||||
|
if (!value) return false;
|
||||||
|
|
||||||
|
var emailParts = value.split("@");
|
||||||
|
|
||||||
|
if (emailParts.length !== 2) return false;
|
||||||
|
|
||||||
|
var account = emailParts[0];
|
||||||
|
var address = emailParts[1];
|
||||||
|
|
||||||
|
if (account.length > 64) return false;
|
||||||
|
else if (address.length > 255) return false;
|
||||||
|
|
||||||
|
var domainParts = address.split(".");
|
||||||
|
|
||||||
|
if (
|
||||||
|
domainParts.some(function (part) {
|
||||||
|
return part.length > 63;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return tester.test(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 {}
|
|
@ -20,9 +20,20 @@ export abstract class FileSystemHelper {
|
||||||
return readFileSync(this.formatPath(...filePath), "base64");
|
return readFileSync(this.formatPath(...filePath), "base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static readRootFile(filePath: string) {
|
||||||
|
return readFileSync(this.normalizePath(process.cwd(), filePath), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
static readTemplateFile(filePath: string) {
|
static readTemplateFile(filePath: string) {
|
||||||
this.createFolder(filePath);
|
return readFileSync(this.normalizePath(process.cwd(), "src", "templates", filePath), "utf8");
|
||||||
return readFileSync(process.cwd() + 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) {
|
static writeFile(filePath: string, filename: string, file: any) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { JWTData, JWTToken } from "../type/jwtTypes";
|
import { JWTData, JWTToken } from "../type/jwtTypes";
|
||||||
import { JWT_SECRET, JWT_EXPIRATION } from "../env.defaults";
|
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import RolePermissionService from "../service/management/rolePermissionService";
|
import RolePermissionService from "../service/management/rolePermissionService";
|
||||||
import UserPermissionService from "../service/management/userPermissionService";
|
import UserPermissionService from "../service/management/userPermissionService";
|
||||||
|
@ -9,11 +8,13 @@ import PermissionHelper from "./permissionHelper";
|
||||||
import WebapiService from "../service/management/webapiService";
|
import WebapiService from "../service/management/webapiService";
|
||||||
import WebapiPermissionService from "../service/management/webapiPermissionService";
|
import WebapiPermissionService from "../service/management/webapiPermissionService";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
import SettingHelper from "./settingsHelper";
|
||||||
|
import { APPLICATION_SECRET } from "../env.defaults";
|
||||||
|
|
||||||
export abstract class JWTHelper {
|
export abstract class JWTHelper {
|
||||||
static validate(token: string): Promise<string | jwt.JwtPayload> {
|
static validate(token: string): Promise<string | jwt.JwtPayload> {
|
||||||
return new Promise<string | jwt.JwtPayload>((resolve, reject) => {
|
return new Promise<string | jwt.JwtPayload>((resolve, reject) => {
|
||||||
jwt.verify(token, JWT_SECRET, (err, decoded) => {
|
jwt.verify(token, APPLICATION_SECRET, (err, decoded) => {
|
||||||
if (err) reject(err.message);
|
if (err) reject(err.message);
|
||||||
else resolve(decoded);
|
else resolve(decoded);
|
||||||
});
|
});
|
||||||
|
@ -27,9 +28,11 @@ export abstract class JWTHelper {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
jwt.sign(
|
jwt.sign(
|
||||||
data,
|
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) => {
|
(err, token) => {
|
||||||
if (err) reject(err.message);
|
if (err) reject(err.message);
|
||||||
|
@ -100,7 +103,8 @@ export abstract class JWTHelper {
|
||||||
};
|
};
|
||||||
|
|
||||||
let overwriteExpiration =
|
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
|
? null
|
||||||
: Date.now() - new Date(expiration).getTime();
|
: Date.now() - new Date(expiration).getTime();
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,76 @@
|
||||||
import { Transporter, createTransport, TransportOptions } from "nodemailer";
|
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 { Attachment } from "nodemailer/lib/mailer";
|
||||||
|
import SettingHelper from "./settingsHelper";
|
||||||
|
import emailCheck from "email-check";
|
||||||
|
|
||||||
export default abstract class MailHelper {
|
export default abstract class MailHelper {
|
||||||
private static readonly transporter: Transporter = createTransport({
|
private static transporter: Transporter;
|
||||||
host: MAIL_HOST,
|
|
||||||
port: MAIL_PORT,
|
static createTransport() {
|
||||||
secure: (MAIL_SECURE as "true" | "false") == "true",
|
this.transporter?.close();
|
||||||
|
|
||||||
|
this.transporter = createTransport({
|
||||||
|
host: SettingHelper.getSetting("mail.host"),
|
||||||
|
port: SettingHelper.getSetting("mail.port"),
|
||||||
|
secure: SettingHelper.getSetting("mail.secure"),
|
||||||
auth: {
|
auth: {
|
||||||
user: MAIL_USERNAME,
|
user: SettingHelper.getSetting("mail.username"),
|
||||||
pass: MAIL_PASSWORD,
|
pass: SettingHelper.getSetting("mail.password"),
|
||||||
},
|
},
|
||||||
} as TransportOptions);
|
} as TransportOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async verifyTransport({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
secure,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
}: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
secure: boolean;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
let transport = createTransport({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
secure,
|
||||||
|
auth: { user, pass: password },
|
||||||
|
});
|
||||||
|
|
||||||
|
return await transport
|
||||||
|
.verify()
|
||||||
|
.then(() => {
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
try {
|
||||||
|
transport?.close();
|
||||||
|
} catch (error) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async checkMail(mail: string): Promise<boolean> {
|
||||||
|
return await emailCheck(mail)
|
||||||
|
.then((res) => {
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static initialize() {
|
||||||
|
SettingHelper.onSettingTopicChanged("mail", () => {
|
||||||
|
this.createTransport();
|
||||||
|
});
|
||||||
|
this.createTransport();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description send mail
|
* @description send mail
|
||||||
|
@ -29,7 +88,7 @@ export default abstract class MailHelper {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.transporter
|
this.transporter
|
||||||
.sendMail({
|
.sendMail({
|
||||||
from: `"${CLUB_NAME}" <${MAIL_USERNAME}>`,
|
from: `"${SettingHelper.getSetting("club.name")}" <${SettingHelper.getSetting("mail.email")}>`,
|
||||||
to: target,
|
to: target,
|
||||||
subject,
|
subject,
|
||||||
text: content,
|
text: content,
|
||||||
|
|
|
@ -10,13 +10,13 @@ import { CalendarHelper } from "./calendarHelper";
|
||||||
import DynamicQueryBuilder from "./dynamicQueryBuilder";
|
import DynamicQueryBuilder from "./dynamicQueryBuilder";
|
||||||
import { FileSystemHelper } from "./fileSystemHelper";
|
import { FileSystemHelper } from "./fileSystemHelper";
|
||||||
import MailHelper from "./mailHelper";
|
import MailHelper from "./mailHelper";
|
||||||
import { CLUB_NAME } from "../env.defaults";
|
|
||||||
import { TemplateHelper } from "./templateHelper";
|
import { TemplateHelper } from "./templateHelper";
|
||||||
import { PdfExport } from "./pdfExport";
|
import { PdfExport } from "./pdfExport";
|
||||||
import NewsletterConfigService from "../service/configuration/newsletterConfigService";
|
import NewsletterConfigService from "../service/configuration/newsletterConfigService";
|
||||||
import { NewsletterConfigType } from "../enums/newsletterConfigType";
|
import { NewsletterConfigEnum } from "../enums/newsletterConfigEnum";
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
|
import SettingHelper from "./settingsHelper";
|
||||||
|
|
||||||
export interface NewsletterEventType {
|
export interface NewsletterEventType {
|
||||||
kind: "pdf" | "mail";
|
kind: "pdf" | "mail";
|
||||||
|
@ -145,7 +145,7 @@ export abstract class NewsletterHelper {
|
||||||
return [];
|
return [];
|
||||||
} else {
|
} else {
|
||||||
let members = await MemberService.getAll({ noLimit: true, ids: queryMemberIds });
|
let members = await MemberService.getAll({ noLimit: true, ids: queryMemberIds });
|
||||||
return members[0];
|
return members[0].filter((m) => m.sendNewsletter != null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,14 +154,11 @@ export abstract class NewsletterHelper {
|
||||||
let recipients = await NewsletterRecipientsService.getAll(newsletterId);
|
let recipients = await NewsletterRecipientsService.getAll(newsletterId);
|
||||||
let config = await NewsletterConfigService.getAll();
|
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 members = await this.transformRecipientsToMembers(newsletter, recipients);
|
||||||
const mailRecipients = members.filter(
|
const mailRecipients = members.filter(
|
||||||
(m) =>
|
(m) => m.sendNewsletter?.email != "" && allowedForMail.includes(m.sendNewsletter?.type?.id)
|
||||||
m.sendNewsletter != null &&
|
|
||||||
m.sendNewsletter?.email != null &&
|
|
||||||
allowedForMail.includes(m.sendNewsletter?.type?.id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return mailRecipients;
|
return mailRecipients;
|
||||||
|
@ -172,17 +169,17 @@ export abstract class NewsletterHelper {
|
||||||
let recipients = await NewsletterRecipientsService.getAll(newsletterId);
|
let recipients = await NewsletterRecipientsService.getAll(newsletterId);
|
||||||
let config = await NewsletterConfigService.getAll();
|
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 members = await this.transformRecipientsToMembers(newsletter, recipients);
|
||||||
const pdfRecipients = members.filter(
|
const pdfRecipients = members.filter((m) => !notAllowedForPdf.includes(m.sendNewsletter?.type?.id));
|
||||||
(m) => !notAllowedForPdf.includes(m.sendNewsletter?.type?.id) || m.sendNewsletter == null
|
|
||||||
);
|
|
||||||
|
|
||||||
pdfRecipients.unshift({
|
pdfRecipients.unshift({
|
||||||
id: "0",
|
id: "0",
|
||||||
firstname: "Alle Mitglieder",
|
firstname: "Alle Mitglieder",
|
||||||
lastname: CLUB_NAME,
|
lastname: SettingHelper.getSetting("club.name"),
|
||||||
nameaffix: "",
|
nameaffix: "",
|
||||||
salutation: { salutation: "" },
|
salutation: { salutation: "" },
|
||||||
} as member);
|
} as member);
|
||||||
|
@ -224,11 +221,14 @@ export abstract class NewsletterHelper {
|
||||||
const { body } = await TemplateHelper.renderFileForModule({
|
const { body } = await TemplateHelper.renderFileForModule({
|
||||||
module: "newsletter",
|
module: "newsletter",
|
||||||
bodyData: data,
|
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, [
|
await MailHelper.sendMail(
|
||||||
{ filename: "events.ics", path: this.getICSFilePath(newsletter) },
|
rec.sendNewsletter.email,
|
||||||
])
|
`Newsletter von ${SettingHelper.getSetting("club.name")}`,
|
||||||
|
body,
|
||||||
|
[{ filename: "events.ics", path: this.getICSFilePath(newsletter) }]
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.formatJobEmit(
|
this.formatJobEmit(
|
||||||
"progress",
|
"progress",
|
||||||
|
@ -278,7 +278,7 @@ export abstract class NewsletterHelper {
|
||||||
if (error) throw new InternalException("Failed Building ICS form Pdf", error);
|
if (error) throw new InternalException("Failed Building ICS form Pdf", error);
|
||||||
this.saveIcsToFile(newsletter, value);
|
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);
|
const pdfRecipients = await this.getPrintRecipients(newsletterId);
|
||||||
|
|
||||||
|
@ -289,7 +289,7 @@ export abstract class NewsletterHelper {
|
||||||
|
|
||||||
await PdfExport.renderFile({
|
await PdfExport.renderFile({
|
||||||
template: "newsletter",
|
template: "newsletter",
|
||||||
title: `Newsletter von ${CLUB_NAME}`,
|
title: `Newsletter von ${SettingHelper.getSetting("club.name")}`,
|
||||||
filename: `${rec.lastname}_${rec.firstname}_${rec.id}`.replaceAll(" ", "-"),
|
filename: `${rec.lastname}_${rec.firstname}_${rec.id}`.replaceAll(" ", "-"),
|
||||||
folder: `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`,
|
folder: `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`,
|
||||||
data: data,
|
data: data,
|
||||||
|
|
259
src/helpers/settingsHelper.ts
Normal file
259
src/helpers/settingsHelper.ts
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
export default abstract class SettingHelper {
|
||||||
|
private static settings: { [key in SettingString]?: string } = {};
|
||||||
|
|
||||||
|
private static listeners: Map<SettingString, Array<(newValue: any, oldValue: any) => void>> = new Map();
|
||||||
|
private static topicListeners: Map<SettingTopic, Array<() => void>> = new Map();
|
||||||
|
|
||||||
|
private static readonly converters: Record<SettingTypeAtom, TypeConverter<any>> = {
|
||||||
|
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<K extends SettingString>(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 = 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
|
||||||
|
* @param key The key of the setting
|
||||||
|
* @param value The value to set
|
||||||
|
*/
|
||||||
|
public static async setSetting<K extends SettingString>(key: K, value: SettingValueMapping[K]): Promise<void> {
|
||||||
|
if (value === undefined || value === null) return;
|
||||||
|
|
||||||
|
const stringValue = String(value);
|
||||||
|
|
||||||
|
const settingType = settingsType[key];
|
||||||
|
this.validateSetting(key, stringValue);
|
||||||
|
|
||||||
|
const oldValue = this.getSetting(key);
|
||||||
|
let finalValue = stringValue;
|
||||||
|
|
||||||
|
if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) {
|
||||||
|
finalValue = CodingHelper.encrypt(APPLICATION_SECRET, stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settings[key] = stringValue;
|
||||||
|
const [topic, settingKey] = key.split(".") as [SettingTopic, string];
|
||||||
|
|
||||||
|
await SettingCommandHandler.create({
|
||||||
|
topic,
|
||||||
|
key: settingKey,
|
||||||
|
value: finalValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newValue = this.getSetting(key);
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let processedValue = valueToCheck;
|
||||||
|
// do not encypt data here - data is only crypted towards database
|
||||||
|
// if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) {
|
||||||
|
// try {
|
||||||
|
// processedValue = CodingHelper.decrypt(APPLICATION_SECRET, processedValue);
|
||||||
|
// } catch (error) {
|
||||||
|
// throw new Error(`Unable to decrypt value for ${key}: ${error.message}`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const baseType =
|
||||||
|
typeof settingType.type === "string"
|
||||||
|
? (settingType.type.split("/")[0] as SettingTypeAtom)
|
||||||
|
: (settingType.type as SettingTypeAtom);
|
||||||
|
|
||||||
|
if (!this.converters[baseType].validate(processedValue)) {
|
||||||
|
throw new Error(`Invalid value for ${key} of type ${baseType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseType === "number" && settingType.min !== undefined) {
|
||||||
|
const numValue = Number(processedValue);
|
||||||
|
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<K extends SettingString>(
|
||||||
|
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<K extends SettingTopic>(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<K extends SettingString>(
|
||||||
|
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 {
|
||||||
|
if (!this.listeners.has(key)) return;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,10 +9,10 @@ export abstract class TemplateHelper {
|
||||||
static getTemplateFromFile(template: string) {
|
static getTemplateFromFile(template: string) {
|
||||||
let tmpFile;
|
let tmpFile;
|
||||||
try {
|
try {
|
||||||
tmpFile = FileSystemHelper.readTemplateFile(`/src/templates/${template}.template.html`);
|
tmpFile = FileSystemHelper.readTemplateFile(`${template}.template.html`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
tmpFile = FileSystemHelper.readTemplateFile(
|
tmpFile = FileSystemHelper.readTemplateFile(
|
||||||
`/src/templates/${template.split(".")[template.split(".").length - 1]}.template.html`
|
`${template.split(".")[template.split(".").length - 1]}.template.html`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return tmpFile;
|
return tmpFile;
|
||||||
|
|
14
src/index.ts
14
src/index.ts
|
@ -2,7 +2,7 @@ import "dotenv/config";
|
||||||
import "./handlebars.config";
|
import "./handlebars.config";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
|
||||||
import { BACKUP_AUTO_RESTORE, configCheck, SERVER_PORT } from "./env.defaults";
|
import { configCheck } from "./env.defaults";
|
||||||
configCheck();
|
configCheck();
|
||||||
|
|
||||||
import { PermissionObject } from "./type/permissionTypes";
|
import { PermissionObject } from "./type/permissionTypes";
|
||||||
|
@ -21,23 +21,29 @@ declare global {
|
||||||
|
|
||||||
import { dataSource } from "./data-source";
|
import { dataSource } from "./data-source";
|
||||||
import BackupHelper from "./helpers/backupHelper";
|
import BackupHelper from "./helpers/backupHelper";
|
||||||
|
import SettingHelper from "./helpers/settingsHelper";
|
||||||
dataSource.initialize().then(async () => {
|
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) => {
|
await BackupHelper.autoRestoreBackup().catch((err) => {
|
||||||
console.log(`${new Date().toISOString()}: failed auto-restoring database`, err);
|
console.log(`${new Date().toISOString()}: failed auto-restoring database`, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await SettingHelper.configure();
|
||||||
|
MailHelper.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
import router from "./routes/index";
|
import router from "./routes/index";
|
||||||
router(app);
|
router(app);
|
||||||
app.listen(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 ? SERVER_PORT : 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 schedule from "node-schedule";
|
||||||
import RefreshCommandHandler from "./command/refreshCommandHandler";
|
import RefreshCommandHandler from "./command/refreshCommandHandler";
|
||||||
|
import MailHelper from "./helpers/mailHelper";
|
||||||
const job = schedule.scheduleJob("0 0 * * *", async () => {
|
const job = schedule.scheduleJob("0 0 * * *", async () => {
|
||||||
console.log(`${new Date().toISOString()}: running Cron`);
|
console.log(`${new Date().toISOString()}: running Cron`);
|
||||||
await RefreshCommandHandler.deleteExpired();
|
await RefreshCommandHandler.deleteExpired();
|
||||||
|
|
27
src/middleware/multer.ts
Normal file
27
src/middleware/multer.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import multer from "multer";
|
||||||
|
import { FileSystemHelper } from "../helpers/fileSystemHelper";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clubImageUpload = clubImageMulter.fields([
|
||||||
|
{ name: "icon", maxCount: 1 },
|
||||||
|
{ name: "logo", maxCount: 1 },
|
||||||
|
]);
|
33
src/migrations/1745059495808-settingsFromEnv.ts
Normal file
33
src/migrations/1745059495808-settingsFromEnv.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
import { setting_table } from "./baseSchemaTables/admin";
|
||||||
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
export class SettingsFromEnv1745059495808 implements MigrationInterface {
|
||||||
|
name = "SettingsFromEnv1745059495808";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.createTable(setting_table, true, true, true);
|
||||||
|
|
||||||
|
//transfer settings of env to database
|
||||||
|
await SettingHelper.setSetting("club.name", process.env.CLUB_NAME);
|
||||||
|
await SettingHelper.setSetting("club.website", process.env.CLUB_WEBSITE);
|
||||||
|
await SettingHelper.setSetting("session.jwt_expiration", process.env.JWT_EXPIRATION as ms.StringValue);
|
||||||
|
await SettingHelper.setSetting("session.refresh_expiration", process.env.REFRESH_EXPIRATION as ms.StringValue);
|
||||||
|
await SettingHelper.setSetting(
|
||||||
|
"session.pwa_refresh_expiration",
|
||||||
|
process.env.PWA_REFRESH_EXPIRATION as ms.StringValue
|
||||||
|
);
|
||||||
|
await SettingHelper.setSetting("mail.username", process.env.MAIL_USERNAME);
|
||||||
|
await SettingHelper.setSetting("mail.password", process.env.MAIL_PASSWORD);
|
||||||
|
await SettingHelper.setSetting("mail.host", process.env.MAIL_HOST);
|
||||||
|
await SettingHelper.setSetting("mail.port", Number(process.env.MAIL_PORT));
|
||||||
|
await SettingHelper.setSetting("mail.secure", Boolean(process.env.MAIL_SECURE));
|
||||||
|
await SettingHelper.setSetting("backup.interval", Number(process.env.BACKUP_INTERVAL));
|
||||||
|
await SettingHelper.setSetting("backup.copies", Number(process.env.BACKUP_COPIES));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropTable(setting_table.name, true, true, true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -148,3 +148,12 @@ export const reset_table = new Table({
|
||||||
{ name: "secret", ...getTypeByORM("varchar") },
|
{ 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") },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { DB_TYPE } from "../env.defaults";
|
||||||
|
|
||||||
export type ORMType = "int" | "bigint" | "boolean" | "date" | "datetime" | "time" | "text" | "varchar" | "uuid";
|
export type ORMType = "int" | "bigint" | "boolean" | "date" | "datetime" | "time" | "text" | "varchar" | "uuid";
|
||||||
export type ORMDefault = "currentTimestamp" | "string" | "boolean" | "number" | "null";
|
export type ORMDefault = "currentTimestamp" | "string" | "boolean" | "number" | "null";
|
||||||
export type ColumnConfig = {
|
export type ColumnConfig = {
|
||||||
|
@ -13,7 +15,7 @@ export type Primary = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getTypeByORM(type: ORMType, nullable: boolean = false, length: number = 255): ColumnConfig {
|
export function getTypeByORM(type: ORMType, nullable: boolean = false, length: number = 255): ColumnConfig {
|
||||||
const dbType = process.env.DB_TYPE;
|
const dbType = DB_TYPE;
|
||||||
|
|
||||||
const typeMap: Record<string, Record<ORMType, string>> = {
|
const typeMap: Record<string, Record<ORMType, string>> = {
|
||||||
mysql: {
|
mysql: {
|
||||||
|
@ -63,7 +65,7 @@ export function getTypeByORM(type: ORMType, nullable: boolean = false, length: n
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultByORM<T = string | null>(type: ORMDefault, data?: string | number | boolean): T {
|
export function getDefaultByORM<T = string | null>(type: ORMDefault, data?: string | number | boolean): T {
|
||||||
const dbType = process.env.DB_TYPE;
|
const dbType = DB_TYPE;
|
||||||
|
|
||||||
const typeMap: Record<string, Record<ORMDefault, string | null>> = {
|
const typeMap: Record<string, Record<ORMDefault, string | null>> = {
|
||||||
mysql: {
|
mysql: {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import user from "./management/user";
|
||||||
import invite from "./management/invite";
|
import invite from "./management/invite";
|
||||||
import api from "./management/webapi";
|
import api from "./management/webapi";
|
||||||
import backup from "./management/backup";
|
import backup from "./management/backup";
|
||||||
|
import setting from "./management/setting";
|
||||||
|
|
||||||
var router = express.Router({ mergeParams: true });
|
var router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
|
@ -159,5 +160,6 @@ router.use(
|
||||||
PermissionHelper.passCheckMiddleware("read", "management", "backup"),
|
PermissionHelper.passCheckMiddleware("read", "management", "backup"),
|
||||||
backup
|
backup
|
||||||
);
|
);
|
||||||
|
router.use("/setting", PermissionHelper.passCheckMiddleware("read", "management", "setting"), setting);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
36
src/routes/admin/management/setting.ts
Normal file
36
src/routes/admin/management/setting.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import express, { Request, Response } from "express";
|
||||||
|
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||||
|
import {
|
||||||
|
getSetting,
|
||||||
|
getSettings,
|
||||||
|
resetSetting,
|
||||||
|
setSetting,
|
||||||
|
} from "../../../controller/admin/management/settingController";
|
||||||
|
|
||||||
|
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.delete(
|
||||||
|
"/:setting",
|
||||||
|
PermissionHelper.passCheckMiddleware("delete", "management", "setting"),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
await resetSetting(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
|
@ -10,7 +10,6 @@ import {
|
||||||
updateUserPermissions,
|
updateUserPermissions,
|
||||||
updateUserRoles,
|
updateUserRoles,
|
||||||
} from "../../../controller/admin/management/userController";
|
} from "../../../controller/admin/management/userController";
|
||||||
import { inviteUser } from "../../../controller/inviteController";
|
|
||||||
|
|
||||||
var router = express.Router({ mergeParams: true });
|
var router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { isSetup } from "../controller/setupController";
|
import { finishInvite, verifyInvite } from "../controller/inviteController";
|
||||||
import { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController";
|
|
||||||
import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper";
|
import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper";
|
||||||
|
|
||||||
var router = express.Router({ mergeParams: true });
|
var router = express.Router({ mergeParams: true });
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import express from "express";
|
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 });
|
var router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
|
@ -7,4 +14,24 @@ router.get("/calendar", async (req, res) => {
|
||||||
await getCalendarItemsByTypes(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;
|
export default router;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Parser from "rss-parser";
|
||||||
var router = express.Router({ mergeParams: true });
|
var router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.get("/version", async (req: Request, res: Response) => {
|
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);
|
let serverJson = JSON.parse(serverPackage);
|
||||||
res.send({
|
res.send({
|
||||||
name: serverJson.name,
|
name: serverJson.name,
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import express from "express";
|
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 { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController";
|
||||||
import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper";
|
import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper";
|
||||||
|
import { clubImageUpload } from "../middleware/multer";
|
||||||
|
|
||||||
var router = express.Router({ mergeParams: true });
|
var router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
|
@ -9,20 +16,44 @@ router.get("/", async (req, res) => {
|
||||||
await isSetup(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) => {
|
router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => {
|
||||||
await verifyInvite(req, res);
|
await verifyInvite(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/",
|
"/me",
|
||||||
ParamaterPassCheckHelper.requiredIncludedMiddleware(["username", "mail", "firstname", "lastname"]),
|
ParamaterPassCheckHelper.requiredIncludedMiddleware(["username", "mail", "firstname", "lastname"]),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
await inviteUser(req, res, false);
|
await inviteUser(req, res, true);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
|
router.post(
|
||||||
|
"/finish",
|
||||||
|
ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]),
|
||||||
|
async (req, res) => {
|
||||||
await finishInvite(req, res, true);
|
await finishInvite(req, res, true);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { Brackets, Like, SelectQueryBuilder } from "typeorm";
|
import { Brackets, Like, SelectQueryBuilder } from "typeorm";
|
||||||
import { dataSource } from "../../../data-source";
|
import { dataSource } from "../../../data-source";
|
||||||
import { member } from "../../../entity/club/member/member";
|
import { member } from "../../../entity/club/member/member";
|
||||||
import { membership } from "../../../entity/club/member/membership";
|
|
||||||
import DatabaseActionException from "../../../exceptions/databaseActionException";
|
import DatabaseActionException from "../../../exceptions/databaseActionException";
|
||||||
import InternalException from "../../../exceptions/internalException";
|
|
||||||
import { memberView } from "../../../views/memberView";
|
import { memberView } from "../../../views/memberView";
|
||||||
import { DB_TYPE } from "../../../env.defaults";
|
import { DB_TYPE } from "../../../env.defaults";
|
||||||
|
|
||||||
|
|
43
src/service/management/settingService.ts
Normal file
43
src/service/management/settingService.ts
Normal file
|
@ -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<setting[]>}
|
||||||
|
*/
|
||||||
|
static async getSettings(): Promise<setting[]> {
|
||||||
|
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<setting>}
|
||||||
|
*/
|
||||||
|
static async getBySettingString(key: SettingString): Promise<setting> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,8 @@ export type PermissionModule =
|
||||||
| "query_store"
|
| "query_store"
|
||||||
| "template"
|
| "template"
|
||||||
| "template_usage"
|
| "template_usage"
|
||||||
| "backup";
|
| "backup"
|
||||||
|
| "setting";
|
||||||
|
|
||||||
export type PermissionType = "read" | "create" | "update" | "delete";
|
export type PermissionType = "read" | "create" | "update" | "delete";
|
||||||
|
|
||||||
|
@ -78,6 +79,7 @@ export const permissionModules: Array<PermissionModule> = [
|
||||||
"template",
|
"template",
|
||||||
"template_usage",
|
"template_usage",
|
||||||
"backup",
|
"backup",
|
||||||
|
"setting",
|
||||||
];
|
];
|
||||||
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
||||||
export const sectionsAndModules: SectionsAndModulesObject = {
|
export const sectionsAndModules: SectionsAndModulesObject = {
|
||||||
|
@ -95,6 +97,6 @@ export const sectionsAndModules: SectionsAndModulesObject = {
|
||||||
"template_usage",
|
"template_usage",
|
||||||
"newsletter_config",
|
"newsletter_config",
|
||||||
],
|
],
|
||||||
management: ["user", "role", "webapi", "backup"],
|
management: ["user", "role", "webapi", "backup", "setting"],
|
||||||
additional: [],
|
additional: [],
|
||||||
};
|
};
|
||||||
|
|
121
src/type/settingTypes.ts
Normal file
121
src/type/settingTypes.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
export type SettingTopic = "club" | "app" | "session" | "mail" | "backup" | "security";
|
||||||
|
export type SettingString =
|
||||||
|
| "club.icon"
|
||||||
|
| "club.logo"
|
||||||
|
| "club.name"
|
||||||
|
| "club.imprint"
|
||||||
|
| "club.privacy"
|
||||||
|
| "club.website"
|
||||||
|
| "app.custom_login_message"
|
||||||
|
| "app.show_link_to_calendar"
|
||||||
|
| "session.jwt_expiration"
|
||||||
|
| "session.refresh_expiration"
|
||||||
|
| "session.pwa_refresh_expiration"
|
||||||
|
| "mail.email"
|
||||||
|
| "mail.username"
|
||||||
|
| "mail.password"
|
||||||
|
| "mail.host"
|
||||||
|
| "mail.port"
|
||||||
|
| "mail.secure"
|
||||||
|
| "backup.interval"
|
||||||
|
| "backup.copies";
|
||||||
|
|
||||||
|
export type SettingTypeAtom = "longstring" | "string" | "ms" | "number" | "boolean" | "url" | "email";
|
||||||
|
export type SettingType = SettingTypeAtom | `${SettingTypeAtom}/crypt` | `${SettingTypeAtom}/rand`;
|
||||||
|
|
||||||
|
export type SettingValueMapping = {
|
||||||
|
"club.icon": string;
|
||||||
|
"club.logo": string;
|
||||||
|
"club.name": string;
|
||||||
|
"club.imprint": string;
|
||||||
|
"club.privacy": string;
|
||||||
|
"club.website": string;
|
||||||
|
"app.custom_login_message": string;
|
||||||
|
"app.show_link_to_calendar": boolean;
|
||||||
|
"session.jwt_expiration": ms.StringValue;
|
||||||
|
"session.refresh_expiration": ms.StringValue;
|
||||||
|
"session.pwa_refresh_expiration": ms.StringValue;
|
||||||
|
"mail.email": string;
|
||||||
|
"mail.username": string;
|
||||||
|
"mail.password": string;
|
||||||
|
"mail.host": string;
|
||||||
|
"mail.port": number;
|
||||||
|
"mail.secure": boolean;
|
||||||
|
"backup.interval": number;
|
||||||
|
"backup.copies": number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Typsicherer Zugriff auf Settings
|
||||||
|
export type SettingDefinition<T extends SettingType | SettingTypeAtom[]> = {
|
||||||
|
type: T;
|
||||||
|
default?: string | number | boolean | ms.StringValue;
|
||||||
|
optional?: boolean;
|
||||||
|
min?: T extends "number" | `number/crypt` | `number/rand` ? number : never;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingsSchema = {
|
||||||
|
[key in SettingString]: SettingDefinition<SettingType | SettingTypeAtom[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const settingsType: SettingsSchema = {
|
||||||
|
"club.icon": { type: "string", optional: true },
|
||||||
|
"club.logo": { type: "string", optional: true },
|
||||||
|
"club.name": { type: "string", default: "FF Admin" },
|
||||||
|
"club.imprint": { type: "url", optional: true },
|
||||||
|
"club.privacy": { type: "url", optional: true },
|
||||||
|
"club.website": { type: "url", optional: true },
|
||||||
|
"app.custom_login_message": { type: "string", optional: true },
|
||||||
|
"app.show_link_to_calendar": { type: "boolean", default: true },
|
||||||
|
"session.jwt_expiration": { type: "ms", default: "15m" },
|
||||||
|
"session.refresh_expiration": { type: "ms", default: "1d" },
|
||||||
|
"session.pwa_refresh_expiration": { type: "ms", default: "5d" },
|
||||||
|
"mail.email": { type: "email", optional: false },
|
||||||
|
"mail.username": { type: "string", optional: false },
|
||||||
|
"mail.password": { type: "string/crypt", optional: false },
|
||||||
|
"mail.host": { type: "url", optional: false },
|
||||||
|
"mail.port": { type: "number", default: 587 },
|
||||||
|
"mail.secure": { type: "boolean", default: false },
|
||||||
|
"backup.interval": { type: "number", default: 1, min: 1 },
|
||||||
|
"backup.copies": { type: "number", default: 7, min: 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** ENV Settings */
|
||||||
|
export type EnvSettingString =
|
||||||
|
| "database.type"
|
||||||
|
| "database.host"
|
||||||
|
| "database.port"
|
||||||
|
| "database.name"
|
||||||
|
| "database.username"
|
||||||
|
| "database.password"
|
||||||
|
| "application.secret"
|
||||||
|
| "security.strict_limit"
|
||||||
|
| "security.strict_limit_window"
|
||||||
|
| "security.strict_limit_request_count"
|
||||||
|
| "security.limit"
|
||||||
|
| "security.limit_window"
|
||||||
|
| "security.limit_request_count"
|
||||||
|
| "security.trust_proxy";
|
||||||
|
|
||||||
|
export const envSettingsType: {
|
||||||
|
[key in EnvSettingString]: {
|
||||||
|
type: SettingType | SettingType[];
|
||||||
|
default?: string | number | boolean;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
"database.type": { type: "string", default: "postgres" },
|
||||||
|
"database.host": { type: "string" },
|
||||||
|
"database.port": { type: "string", default: "5432" },
|
||||||
|
"database.name": { type: "string" },
|
||||||
|
"database.username": { type: "string" },
|
||||||
|
"database.password": { type: "string" },
|
||||||
|
"application.secret": { type: "string" },
|
||||||
|
"security.strict_limit": { type: "boolean", default: true },
|
||||||
|
"security.strict_limit_window": { type: "ms", default: "15m" },
|
||||||
|
"security.strict_limit_request_count": { type: "number", default: 15 },
|
||||||
|
"security.limit": { type: "boolean", default: true },
|
||||||
|
"security.limit_window": { type: "ms", default: "1m" },
|
||||||
|
"security.limit_request_count": { type: "number", default: 500 },
|
||||||
|
"security.trust_proxy": { type: ["boolean", "number", "string"] },
|
||||||
|
};
|
|
@ -1,8 +1,8 @@
|
||||||
import { NewsletterConfigType } from "../../../enums/newsletterConfigType";
|
import { NewsletterConfigEnum } from "../../../enums/newsletterConfigEnum";
|
||||||
import { CommunicationTypeViewModel } from "./communicationType.models";
|
import { CommunicationTypeViewModel } from "./communicationType.models";
|
||||||
|
|
||||||
export interface NewsletterConfigViewModel {
|
export interface NewsletterConfigViewModel {
|
||||||
comTypeId: number;
|
comTypeId: number;
|
||||||
config: NewsletterConfigType;
|
config: NewsletterConfigEnum;
|
||||||
comType: CommunicationTypeViewModel;
|
comType: CommunicationTypeViewModel;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue