From 1d56c7f798bc7cf8ff988d9d326d36be7c51952b Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Sun, 16 Feb 2025 10:48:12 +0100 Subject: [PATCH] base structure transfered from ff admin --- .dockerignore | 6 + .env.example | 47 + .gitignore | 4 + .prettierrc | 5 + Dockerfile | 46 + README.md | 119 +- package-lock.json | 4829 +++++++++++++++++ package.json | 74 + .../configuration/member/memberCommand.ts | 22 + .../member/memberCommandHandler.ts | 70 + src/command/management/role/roleCommand.ts | 12 + .../management/role/roleCommandHandler.ts | 67 + .../management/role/rolePermissionCommand.ts | 16 + .../role/rolePermissionCommandHandler.ts | 75 + src/command/management/user/inviteCommand.ts | 12 + .../management/user/inviteCommandHandler.ts | 75 + src/command/management/user/userCommand.ts | 35 + .../management/user/userCommandHandler.ts | 170 + .../management/user/userPermissionCommand.ts | 16 + .../user/userPermissionCommandHandler.ts | 76 + src/command/refreshCommand.ts | 9 + src/command/refreshCommandHandler.ts | 74 + src/command/resetCommand.ts | 10 + src/command/resetCommandHandler.ts | 55 + .../admin/configuration/memberController.ts | 137 + .../admin/management/backupController.ts | 126 + .../admin/management/roleController.ts | 121 + .../admin/management/userController.ts | 166 + src/controller/authController.ts | 98 + src/controller/inviteController.ts | 175 + src/controller/resetController.ts | 124 + src/controller/setupController.ts | 11 + src/controller/userController.ts | 121 + src/data-source.ts | 34 + src/entity/configuration/member.ts | 16 + src/entity/management/invite.ts | 22 + src/entity/management/role.ts | 22 + src/entity/management/role_permission.ts | 19 + src/entity/management/user.ts | 44 + src/entity/management/user_permission.ts | 19 + src/entity/management/webapi.ts | 27 + src/entity/management/webapi_permission.ts | 19 + src/entity/refresh.ts | 22 + src/entity/reset.ts | 16 + src/env.defaults.ts | 116 + src/exceptions/badRequestException.ts | 7 + src/exceptions/customRequestException.ts | 12 + src/exceptions/databaseActionException.ts | 8 + src/exceptions/exceptionsBaseType.ts | 3 + src/exceptions/forbiddenRequestException.ts | 7 + src/exceptions/internalException.ts | 7 + .../unauthorizedRequestException.ts | 7 + src/factory/admin/configuration/member.ts | 27 + src/factory/admin/management/invite.ts | 27 + src/factory/admin/management/role.ts | 27 + src/factory/admin/management/user.ts | 39 + src/helpers/backupHelper.ts | 246 + src/helpers/fileSystemHelper.ts | 71 + src/helpers/jwtHelper.ts | 78 + src/helpers/mailHelper.ts | 43 + src/helpers/parameterPassCheckHelper.ts | 28 + src/helpers/permissionHelper.ts | 255 + src/helpers/stringHelper.ts | 17 + src/index.ts | 44 + src/middleware/allowSetup.ts | 12 + src/middleware/authenticate.ts | 43 + src/middleware/detectPWA.ts | 11 + src/middleware/errorHandler.ts | 22 + src/migrations/1739697068682-CreateSchema.ts | 42 + src/migrations/baseSchemaTables/admin.ts | 121 + src/migrations/baseSchemaTables/member.ts | 12 + src/migrations/ormHelper.ts | 105 + src/routes/admin/configuration/member.ts | 50 + src/routes/admin/index.ts | 27 + src/routes/admin/management/backup.ts | 88 + src/routes/admin/management/invite.ts | 27 + src/routes/admin/management/role.ts | 59 + src/routes/admin/management/user.ts | 65 + src/routes/auth.ts | 18 + src/routes/index.ts | 88 + src/routes/invite.ts | 16 + src/routes/reset.ts | 19 + src/routes/server.ts | 35 + src/routes/setup.ts | 28 + src/routes/user.ts | 26 + src/service/configuration/memberService.ts | 80 + src/service/management/inviteService.ts | 44 + .../management/rolePermissionService.ts | 45 + src/service/management/roleService.ts | 45 + .../management/userPermissionService.ts | 25 + src/service/management/userService.ts | 132 + src/service/refreshService.ts | 26 + src/service/resetService.ts | 27 + src/type/jwtTypes.ts | 19 + src/type/permissionTypes.ts | 33 + .../admin/configuration/member.models.ts | 6 + .../admin/management/invite.models.ts | 6 + src/viewmodel/admin/management/role.models.ts | 7 + src/viewmodel/admin/management/user.models.ts | 14 + src/viewmodel/permissionViewModel.ts | 1 + tsconfig.json | 19 + 101 files changed, 9773 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .prettierrc create mode 100644 Dockerfile create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/command/configuration/member/memberCommand.ts create mode 100644 src/command/configuration/member/memberCommandHandler.ts create mode 100644 src/command/management/role/roleCommand.ts create mode 100644 src/command/management/role/roleCommandHandler.ts create mode 100644 src/command/management/role/rolePermissionCommand.ts create mode 100644 src/command/management/role/rolePermissionCommandHandler.ts create mode 100644 src/command/management/user/inviteCommand.ts create mode 100644 src/command/management/user/inviteCommandHandler.ts create mode 100644 src/command/management/user/userCommand.ts create mode 100644 src/command/management/user/userCommandHandler.ts create mode 100644 src/command/management/user/userPermissionCommand.ts create mode 100644 src/command/management/user/userPermissionCommandHandler.ts create mode 100644 src/command/refreshCommand.ts create mode 100644 src/command/refreshCommandHandler.ts create mode 100644 src/command/resetCommand.ts create mode 100644 src/command/resetCommandHandler.ts create mode 100644 src/controller/admin/configuration/memberController.ts create mode 100644 src/controller/admin/management/backupController.ts create mode 100644 src/controller/admin/management/roleController.ts create mode 100644 src/controller/admin/management/userController.ts create mode 100644 src/controller/authController.ts create mode 100644 src/controller/inviteController.ts create mode 100644 src/controller/resetController.ts create mode 100644 src/controller/setupController.ts create mode 100644 src/controller/userController.ts create mode 100644 src/data-source.ts create mode 100644 src/entity/configuration/member.ts create mode 100644 src/entity/management/invite.ts create mode 100644 src/entity/management/role.ts create mode 100644 src/entity/management/role_permission.ts create mode 100644 src/entity/management/user.ts create mode 100644 src/entity/management/user_permission.ts create mode 100644 src/entity/management/webapi.ts create mode 100644 src/entity/management/webapi_permission.ts create mode 100644 src/entity/refresh.ts create mode 100644 src/entity/reset.ts create mode 100644 src/env.defaults.ts create mode 100644 src/exceptions/badRequestException.ts create mode 100644 src/exceptions/customRequestException.ts create mode 100644 src/exceptions/databaseActionException.ts create mode 100644 src/exceptions/exceptionsBaseType.ts create mode 100644 src/exceptions/forbiddenRequestException.ts create mode 100644 src/exceptions/internalException.ts create mode 100644 src/exceptions/unauthorizedRequestException.ts create mode 100644 src/factory/admin/configuration/member.ts create mode 100644 src/factory/admin/management/invite.ts create mode 100644 src/factory/admin/management/role.ts create mode 100644 src/factory/admin/management/user.ts create mode 100644 src/helpers/backupHelper.ts create mode 100644 src/helpers/fileSystemHelper.ts create mode 100644 src/helpers/jwtHelper.ts create mode 100644 src/helpers/mailHelper.ts create mode 100644 src/helpers/parameterPassCheckHelper.ts create mode 100644 src/helpers/permissionHelper.ts create mode 100644 src/helpers/stringHelper.ts create mode 100644 src/index.ts create mode 100644 src/middleware/allowSetup.ts create mode 100644 src/middleware/authenticate.ts create mode 100644 src/middleware/detectPWA.ts create mode 100644 src/middleware/errorHandler.ts create mode 100644 src/migrations/1739697068682-CreateSchema.ts create mode 100644 src/migrations/baseSchemaTables/admin.ts create mode 100644 src/migrations/baseSchemaTables/member.ts create mode 100644 src/migrations/ormHelper.ts create mode 100644 src/routes/admin/configuration/member.ts create mode 100644 src/routes/admin/index.ts create mode 100644 src/routes/admin/management/backup.ts create mode 100644 src/routes/admin/management/invite.ts create mode 100644 src/routes/admin/management/role.ts create mode 100644 src/routes/admin/management/user.ts create mode 100644 src/routes/auth.ts create mode 100644 src/routes/index.ts create mode 100644 src/routes/invite.ts create mode 100644 src/routes/reset.ts create mode 100644 src/routes/server.ts create mode 100644 src/routes/setup.ts create mode 100644 src/routes/user.ts create mode 100644 src/service/configuration/memberService.ts create mode 100644 src/service/management/inviteService.ts create mode 100644 src/service/management/rolePermissionService.ts create mode 100644 src/service/management/roleService.ts create mode 100644 src/service/management/userPermissionService.ts create mode 100644 src/service/management/userService.ts create mode 100644 src/service/refreshService.ts create mode 100644 src/service/resetService.ts create mode 100644 src/type/jwtTypes.ts create mode 100644 src/type/permissionTypes.ts create mode 100644 src/viewmodel/admin/configuration/member.models.ts create mode 100644 src/viewmodel/admin/management/invite.models.ts create mode 100644 src/viewmodel/admin/management/role.models.ts create mode 100644 src/viewmodel/admin/management/user.models.ts create mode 100644 src/viewmodel/permissionViewModel.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9ede668 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +# NodeJs +node_modules/ +dist/ +.git/ +files/ +.env \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0d9bf9d --- /dev/null +++ b/.env.example @@ -0,0 +1,47 @@ +DB_TYPE = (mysql|sqlite|postgres) # default ist mysql + +## BSP für mysql +DB_PORT = 3306 +DB_HOST = database_host +DB_NAME = database_name +DB_USERNAME = database_username +DB_PASSWORD = database_password + +## BSP für postgres +DB_PORT = 5432 +DB_HOST = database_host +DB_NAME = database_name +DB_USERNAME = database_username +DB_PASSWORD = database_password + +## BSP für sqlite +DB_HOST = filename.db + +SERVER_PORT = portnumber + +JWT_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 # besitzt default +JWT_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 15m +REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 1d +PWA_REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 5d + +MAIL_USERNAME = mail_username +MAIL_PASSWORD = mail_password +MAIL_HOST = mail_hoststring +MAIL_PORT = mail_portnumber # default ist 587 +MAIL_SECURE = (true|false) # true für port 465, false für anders gewählten port + +CLUB_NAME = clubname #default FF Admin +CLUB_WEBSITE = https://my-club-website-url #optional, muss aber mit http:// oder https:// beginnen + +BACKUP_INTERVAL = number of days (min 1) # default 1 +BACKUP_COPIES = number of parallel copies # default 7 +BACKUP_AUTO_RESTORE = (true|false) # default ist true + +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_REQUEST_COUNT = strict_request_count # default ist 15 +USE_SECURITY_LIMIT = (true|false) # default ist true +SECURITY_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 1m +SECURITY_LIMIT_REQUEST_COUNT = request_count # default ist 500 + +TRUST_PROXY = # wenn leer, wird dieser Wert nicht angewendet. \ No newline at end of file diff --git a/.gitignore b/.gitignore index ceaea36..7410d05 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,7 @@ dist .yarn/install-state.gz .pnp.* +files + +.idea +*.db \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c4795ed --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "printWidth": 120 +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cba8aa0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM node:18-alpine AS build + +RUN apk add --no-cache \ + chromium \ + nss \ + freetype \ + harfbuzz \ + ca-certificates \ + ttf-freefont + +WORKDIR /app + +COPY package*.json ./ + +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + +RUN npm install + +COPY . /app + +RUN npm run build + +FROM node:18-alpine AS prod + +RUN apk add --no-cache \ + chromium \ + nss \ + freetype \ + harfbuzz \ + ca-certificates \ + ttf-freefont + +WORKDIR /app + +RUN mkdir -p /app/files + +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + +COPY --from=build /app/src/templates /app/src/templates +COPY --from=build /app/dist /app/dist +COPY --from=build /app/node_modules /app/node_modules +COPY --from=build /app/package.json /app/package.json + +EXPOSE 5000 + +CMD [ "npm", "run", "start" ] \ No newline at end of file diff --git a/README.md b/README.md index 9a21bf2..91f6037 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,118 @@ -# ff-operation-server +# ff-admin-server -Einsatzverwaltung für Feuerwehren und Vereine. \ No newline at end of file +Administration für Feuerwehren und Vereine (Backend). + +## Einleitung + +Dieses Projekt, `ff-admin-server`, ist das Backend zur Verwaltung von Mitgliederdaten. Die zugehörige Webapp ist im Repository [ff-admin-ui](https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin) zu finden. + +Eine Demo zusammen mit der `ff-admin` finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de). + +## Installation + +Das Image exposed nur den Port 5000. Die Env-Variable SERVER_PORT kann nur im lokal ausführenden dev-Kontext verwendet werden. + +### Docker Compose Setup + +Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit folgendem Inhalt: + +```yaml +version: "3" + +services: + ff-admin-server: + image: docker.registry.jk-effects.cloud/ehrenamt/ff-admin/server:latest + container_name: ff_member_administration_server + restart: unless-stopped + environment: + - DB_TYPE= # default ist auf mysql gesetzt + - DB_HOST=ff-db + - DB_PORT= # default ist auf 3306 gesetzt + - DB_NAME=ffadmin + - DB_USERNAME=administration_backend + - DB_PASSWORD= + - JWT_SECRET= + - JWT_EXPIRATION= # default ist auf 15m gesetzt + - REFRESH_EXPIRATION= # default ist auf 1d gesetzt + - PWA_REFRESH_EXPIRATION= # default ist auf 5d gesetzt + - MAIL_USERNAME= + - MAIL_PASSWORD= + - MAIL_HOST= + - MAIL_PORT= # default ist auf 587 gesetzt + - MAIL_SECURE= # default ist auf false gesetzt + - CLUB_NAME= # default ist auf FF Admin gesetzt + - CLUB_WEBSITE= + - BACKUP_INTERVAL= # alle x Tage, sonst keine + - BACKUP_COPIES= # Anzahl parallel bestehender Backups + - BACKUP_AUTO_RESTORE= # default ist auf true gesetzt + - USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true + - SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15 + - SECURITY_STRICT_LIMIT_REQUEST_COUNT = strict_request_count # default ist 15 + - USE_SECURITY_LIMIT = (true|false) # default ist true + - SECURITY_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 1m + - SECURITY_LIMIT_REQUEST_COUNT = request_count # default ist 500 + - TRUST_PROXY = # wenn leer, wird dieser Wert nicht angewendet. + volumes: + - :/app/files + networks: + - ff_internal + depends_on: + - ff-db + + ff-db: + image: mariadb:11.2 + container_name: ff_db + restart: unless-stopped + environment: + - MYSQL_DATABASE=ffadmin + - MYSQL_USER=administration_backend + - MYSQL_PASSWORD= + - MYSQL_ROOT_PASSWORD= + volumes: + - :/var/lib/mysql + networks: + - ff_internal + # OR + image: postgres:16 + container_name: ff_db + restart: unless-stopped + environment: + - POSTGRES_DB=ffadmin + - POSTGRES_USER=administration_backend + - POSTGRES_PASSWORD= + volumes: + - :/var/lib/postgresql/data + networks: + - ff_internal + +networks: + ff_internal: +``` + +Die Verwendung von postgres wird aufgrund des Verhaltens bei Datenbank-Update-Fehlern empfohlen. + +Die Verwendung von SQLite wird nur für die Entwicklung oder lokale Tests empfohlen. + +Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten: + +```sh +docker-compose up -d +``` + +### Manuelle Installation + +Klonen Sie dieses Repository und installieren Sie die Abhängigkeiten: + +```sh +git clone https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin-server.git +cd ff-admin-server +npm install +npm run build +npm run start +``` + +## Fragen und Wünsche + +Bei Fragen, Anregungen oder Wünschen können Sie sich gerne melden.\ +Wir freuen uns über Ihr Feedback und helfen Ihnen gerne weiter.\ +Schreiben Sie dafür eine Mail an julian.krauser@jk-effects.com. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..217280a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4829 @@ +{ + "name": "ff-operation-server", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ff-operation-server", + "version": "0.0.0", + "license": "AGPL-3.0-only", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^5.0.0-beta.3", + "express-rate-limit": "^7.5.0", + "express-validator": "^7.2.1", + "handlebars": "^4.7.8", + "helmet": "^8.0.0", + "ics": "^3.8.1", + "ip": "^2.0.1", + "jsonwebtoken": "^9.0.2", + "lodash.uniqby": "^4.7.0", + "moment": "^2.30.1", + "morgan": "^1.10.0", + "ms": "^2.1.3", + "multer": "^1.4.5-lts.1", + "mysql": "^2.18.1", + "node-schedule": "^2.1.1", + "nodemailer": "^6.10.0", + "pg": "^8.13.1", + "qrcode": "^1.5.4", + "reflect-metadata": "^0.2.2", + "rss-parser": "^3.13.0", + "socket.io": "^4.7.5", + "speakeasy": "^2.0.0", + "sqlite3": "^5.1.7", + "typeorm": "^0.3.20", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@types/cors": "^2.8.14", + "@types/express": "^4.17.17", + "@types/ip": "^1.1.3", + "@types/jsonwebtoken": "^9.0.6", + "@types/lodash.uniqby": "^4.7.9", + "@types/morgan": "^1.9.9", + "@types/ms": "^0.7.34", + "@types/multer": "^1.4.12", + "@types/mysql": "^2.15.21", + "@types/node": "^16.18.41", + "@types/node-schedule": "^2.1.6", + "@types/nodemailer": "^6.4.17", + "@types/qrcode": "~1.5.5", + "@types/speakeasy": "^2.0.10", + "@types/uuid": "^9.0.2", + "ts-node": "10.7.0", + "typescript": "^4.5.2" + } + }, + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "devOptional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "devOptional": true, + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "devOptional": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/ip": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.3.tgz", + "integrity": "sha512-64waoJgkXFTYnCYDUWgSATJ/dXEBanVkaP5d4Sbk7P6U7cTTMhxVyROTckc6JKdwCrgnAjZMn0k3177aQxtDEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash.uniqby": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/lodash.uniqby/-/lodash.uniqby-4.7.9.tgz", + "integrity": "sha512-rjrXji/seS6BZJRgXrU2h6FqxRVufsbq/HE0Tx0SdgbtlWr2YmD/M64BlYEYYlaMcpZwy32IYVkMfUMYlPuv0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/morgan": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", + "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/mysql": { + "version": "2.15.26", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", + "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "16.18.105", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.105.tgz", + "integrity": "sha512-w2d0Z9yMk07uH3+Cx0N8lqFyi3yjXZxlbYappPj+AsOlT02OyxyiuNoNHdGt6EuiSm8Wtgp2YV7vWg+GMFrvFA==" + }, + "node_modules/@types/node-schedule": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.7.tgz", + "integrity": "sha512-G7Z3R9H7r3TowoH6D2pkzUHPhcJrDF4Jz1JOQ80AX0K2DWTHoN9VC94XzFAPNMdbW9TBzMZ3LjpFi7RYdbxtXA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/speakeasy": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/speakeasy/-/speakeasy-2.0.10.tgz", + "integrity": "sha512-QVRlDW5r4yl7p7xkNIbAIC/JtyOcClDIIdKfuG7PWdDT1MmyhtXSANsildohy0K+Lmvf/9RUtLbNLMacvrVwxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "devOptional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "devOptional": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true + }, + "node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base32.js": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz", + "integrity": "sha512-EGHIRiegFa62/SsA1J+Xs2tIzludPdzM064N9wjbiEgHnGnJ1V0WEpA4pEwCYT5nDvZk3ubf0shqaCS7k6xeUQ==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz", + "integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.0.0-beta.2.tgz", + "integrity": "sha512-oxdqeGYQcO5ovwwkC1A89R0Mf0v3+7smTVh0chGfzDeiK37bg5bYNtXDy3Nmzn6CShoIYk5+nHTyBoSZIWwnCA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "3.1.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.5.2", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "3.0.0-beta.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dayjs": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", + "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.0.0-beta.3", + "resolved": "https://registry.npmjs.org/express/-/express-5.0.0-beta.3.tgz", + "integrity": "sha512-e7Qizw4gMBVe1Ky2oNi5C1h6oS8aWDcY2yYxvRMy5aMc6t2aqobuHpQRfR3LRC9NAW/c6081SeGWMGBorLXePg==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "3.0.0", + "body-parser": "2.0.0-beta.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "3.1.0", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "mime-types": "~2.1.34", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-is-absolute": "1.0.1", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "router": "2.0.0-beta.2", + "safe-buffer": "5.2.1", + "send": "1.0.0-beta.2", + "serve-static": "2.0.0-beta.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express-validator": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", + "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.0.0.tgz", + "integrity": "sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "engines": { + "node": "*" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", + "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/ics/-/ics-3.8.1.tgz", + "integrity": "sha512-UqQlfkajfhrS4pUGQfGIJMYz/Jsl/ob3LqcfEhUmLbwumg+ZNkU0/6S734Vsjq3/FYNpEcZVKodLBoe+zBM69g==", + "license": "ISC", + "dependencies": { + "nanoid": "^3.1.23", + "runes2": "^1.1.2", + "yup": "^1.2.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "license": "MIT" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "license": "MIT" + }, + "node_modules/long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "dependencies": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.65.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz", + "integrity": "sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-schedule": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz", + "integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==", + "dependencies": { + "cron-parser": "^4.2.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/nodemailer": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", + "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", + "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + }, + "node_modules/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0-beta.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0-beta.1.tgz", + "integrity": "sha512-XlSTHr67bCjSo5aOfAnN3x507zGvi3unF65BW57limYkc2ws/XB0mLUtJvvP7JGFeSPsYrlCv1ZrPGh0cwDxPQ==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.5.2", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/router": { + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/router/-/router-2.0.0-beta.2.tgz", + "integrity": "sha512-ascmzrv4IAB64SpWzFwYOA+jz6PaUbrzHLPsQrPjQ3uQTL2qlhwY9S2sRvvBMgUISQptQG457jcWWcWqtwrbag==", + "dependencies": { + "array-flatten": "3.0.0", + "is-promise": "4.0.0", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "3.2.0", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rss-parser": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.13.0.tgz", + "integrity": "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==", + "license": "MIT", + "dependencies": { + "entities": "^2.0.3", + "xml2js": "^0.5.0" + } + }, + "node_modules/runes2": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz", + "integrity": "sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/send/-/send-1.0.0-beta.2.tgz", + "integrity": "sha512-k1yHu/FNK745PULKdsGpQ+bVSXYNwSk+bWnYzbxGZbt5obZc0JKDVANsCRuJD1X/EG15JtP9eZpwxkhUxIYEcg==", + "dependencies": { + "debug": "3.1.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime-types": "~2.1.34", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/send/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-static": { + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.0.0-beta.2.tgz", + "integrity": "sha512-Ge718g4UJjzYoXFEGLY/VLSuTHp0kQcUV65QA98J8d3XREsVIHu53GBh9NWjDy4u2xwsSwRzu9nu7Q+b4o6Xyw==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "^1.0.0-beta.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakeasy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", + "integrity": "sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw==", + "license": "MIT", + "dependencies": { + "base32.js": "0.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", + "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", + "devOptional": true, + "dependencies": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.0", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz", + "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "chalk": "^4.1.2", + "cli-highlight": "^2.1.11", + "dayjs": "^1.11.9", + "debug": "^4.3.4", + "dotenv": "^16.0.3", + "glob": "^10.3.10", + "mkdirp": "^2.1.3", + "reflect-metadata": "^0.2.1", + "sha.js": "^2.4.11", + "tslib": "^2.5.0", + "uuid": "^9.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0", + "@sap/hana-client": "^2.12.25", + "better-sqlite3": "^7.1.2 || ^8.0.0 || ^9.0.0", + "hdb-pool": "^0.1.6", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0", + "mssql": "^9.1.1 || ^10.0.1", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "hdb-pool": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/typeorm/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/typeorm/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/typeorm/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/typeorm/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/typeorm/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/typeorm/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..900635a --- /dev/null +++ b/package.json @@ -0,0 +1,74 @@ +{ + "name": "ff-operation-server", + "version": "0.0.0", + "description": "Feuerwehr/Verein Einsatzverwaltung Server", + "main": "dist/index.js", + "scripts": { + "start_ts": "ts-node src/index.ts", + "typeorm": "typeorm-ts-node-commonjs", + "migrate": "set DBMODE=migration && npx typeorm-ts-node-commonjs migration:generate ./src/migrations/%npm_config_name% -d ./src/data-source.ts", + "synchronize-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs schema:sync -d ./src/data-source.ts", + "update-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts", + "revert-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:revert -d ./src/data-source.ts", + "build": "tsc", + "start": "node .", + "dev": "npm run build && set NODE_ENV=development && npm run start" + }, + "repository": { + "type": "git", + "url": "https://forgejo.jk-effects.cloud/Ehrenamt/ff-operation-server.git" + }, + "keywords": [ + "Feuerwehr" + ], + "author": "JK Effects", + "license": "AGPL-3.0-only", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^5.0.0-beta.3", + "express-rate-limit": "^7.5.0", + "express-validator": "^7.2.1", + "handlebars": "^4.7.8", + "helmet": "^8.0.0", + "ics": "^3.8.1", + "ip": "^2.0.1", + "jsonwebtoken": "^9.0.2", + "lodash.uniqby": "^4.7.0", + "moment": "^2.30.1", + "morgan": "^1.10.0", + "ms": "^2.1.3", + "multer": "^1.4.5-lts.1", + "mysql": "^2.18.1", + "node-schedule": "^2.1.1", + "nodemailer": "^6.10.0", + "pg": "^8.13.1", + "qrcode": "^1.5.4", + "reflect-metadata": "^0.2.2", + "rss-parser": "^3.13.0", + "socket.io": "^4.7.5", + "speakeasy": "^2.0.0", + "sqlite3": "^5.1.7", + "typeorm": "^0.3.20", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@types/cors": "^2.8.14", + "@types/express": "^4.17.17", + "@types/ip": "^1.1.3", + "@types/jsonwebtoken": "^9.0.6", + "@types/lodash.uniqby": "^4.7.9", + "@types/morgan": "^1.9.9", + "@types/ms": "^0.7.34", + "@types/multer": "^1.4.12", + "@types/mysql": "^2.15.21", + "@types/node": "^16.18.41", + "@types/node-schedule": "^2.1.6", + "@types/nodemailer": "^6.4.17", + "@types/qrcode": "~1.5.5", + "@types/speakeasy": "^2.0.10", + "@types/uuid": "^9.0.2", + "ts-node": "10.7.0", + "typescript": "^4.5.2" + } +} diff --git a/src/command/configuration/member/memberCommand.ts b/src/command/configuration/member/memberCommand.ts new file mode 100644 index 0000000..f5c03d1 --- /dev/null +++ b/src/command/configuration/member/memberCommand.ts @@ -0,0 +1,22 @@ +export interface CreateMemberCommand { + salutationId: number; + firstname: string; + lastname: string; + nameaffix: string; + birthdate: Date; + internalId?: string; +} + +export interface UpdateMemberCommand { + id: string; + salutationId: number; + firstname: string; + lastname: string; + nameaffix: string; + birthdate: Date; + internalId?: string; +} + +export interface DeleteMemberCommand { + id: string; +} diff --git a/src/command/configuration/member/memberCommandHandler.ts b/src/command/configuration/member/memberCommandHandler.ts new file mode 100644 index 0000000..0b0eb69 --- /dev/null +++ b/src/command/configuration/member/memberCommandHandler.ts @@ -0,0 +1,70 @@ +import { dataSource } from "../../../data-source"; +import { member } from "../../../entity/configuration/member"; +import DatabaseActionException from "../../../exceptions/databaseActionException"; +import { CreateMemberCommand, DeleteMemberCommand, UpdateMemberCommand } from "./memberCommand"; + +export default abstract class MemberCommandHandler { + /** + * @description create member + * @param {CreateMemberCommand} createMember + * @returns {Promise} + */ + static async create(createMember: CreateMemberCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(member) + .values({ + firstname: createMember.firstname, + lastname: createMember.lastname, + nameaffix: createMember.nameaffix, + }) + .execute() + .then((result) => { + return result.identifiers[0].id; + }) + .catch((err) => { + throw new DatabaseActionException("CREATE", "member", err); + }); + } + + /** + * @description update member + * @param {UpdateMemberCommand} updateMember + * @returns {Promise} + */ + static async update(updateMember: UpdateMemberCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(member) + .set({ + firstname: updateMember.firstname, + lastname: updateMember.lastname, + nameaffix: updateMember.nameaffix, + }) + .where("id = :id", { id: updateMember.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("UPDATE", "member", err); + }); + } + + /** + * @description delete member + * @param {DeleteMemberCommand} deleteMember + * @returns {Promise} + */ + static async delete(deleteMember: DeleteMemberCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(member) + .where("id = :id", { id: deleteMember.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "member", err); + }); + } +} diff --git a/src/command/management/role/roleCommand.ts b/src/command/management/role/roleCommand.ts new file mode 100644 index 0000000..7638c8b --- /dev/null +++ b/src/command/management/role/roleCommand.ts @@ -0,0 +1,12 @@ +export interface CreateRoleCommand { + role: string; +} + +export interface UpdateRoleCommand { + id: number; + role: string; +} + +export interface DeleteRoleCommand { + id: number; +} diff --git a/src/command/management/role/roleCommandHandler.ts b/src/command/management/role/roleCommandHandler.ts new file mode 100644 index 0000000..0a12e1f --- /dev/null +++ b/src/command/management/role/roleCommandHandler.ts @@ -0,0 +1,67 @@ +import { dataSource } from "../../../data-source"; +import { role } from "../../../entity/management/role"; +import DatabaseActionException from "../../../exceptions/databaseActionException"; +import InternalException from "../../../exceptions/internalException"; +import { CreateRoleCommand, DeleteRoleCommand, UpdateRoleCommand } from "./roleCommand"; + +export default abstract class RoleCommandHandler { + /** + * @description create role + * @param {CreateRoleCommand} createRole + * @returns {Promise} + */ + static async create(createRole: CreateRoleCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(role) + .values({ + role: createRole.role, + }) + .execute() + .then((result) => { + return result.identifiers[0].id; + }) + .catch((err) => { + throw new DatabaseActionException("CREATE", "role", err); + }); + } + + /** + * @description update role + * @param {UpdateRoleCommand} updateRole + * @returns {Promise} + */ + static async update(updateRole: UpdateRoleCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(role) + .set({ + role: updateRole.role, + }) + .where("id = :id", { id: updateRole.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("UPDATE", "role", err); + }); + } + + /** + * @description delete role + * @param {DeleteRoleCommand} deleteRole + * @returns {Promise} + */ + static async delete(deleteRole: DeleteRoleCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(role) + .where("id = :id", { id: deleteRole.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "role", err); + }); + } +} diff --git a/src/command/management/role/rolePermissionCommand.ts b/src/command/management/role/rolePermissionCommand.ts new file mode 100644 index 0000000..3d4021c --- /dev/null +++ b/src/command/management/role/rolePermissionCommand.ts @@ -0,0 +1,16 @@ +import { PermissionString } from "../../../type/permissionTypes"; + +export interface CreateRolePermissionCommand { + permission: PermissionString; + roleId: number; +} + +export interface DeleteRolePermissionCommand { + permission: PermissionString; + roleId: number; +} + +export interface UpdateRolePermissionsCommand { + roleId: number; + permissions: Array; +} diff --git a/src/command/management/role/rolePermissionCommandHandler.ts b/src/command/management/role/rolePermissionCommandHandler.ts new file mode 100644 index 0000000..8a9230e --- /dev/null +++ b/src/command/management/role/rolePermissionCommandHandler.ts @@ -0,0 +1,75 @@ +import { DeleteResult, EntityManager, InsertResult } from "typeorm"; +import { dataSource } from "../../../data-source"; +import { rolePermission } from "../../../entity/management/role_permission"; +import InternalException from "../../../exceptions/internalException"; +import RoleService from "../../../service/management/roleService"; +import { + CreateRolePermissionCommand, + DeleteRolePermissionCommand, + UpdateRolePermissionsCommand, +} from "./rolePermissionCommand"; +import PermissionHelper from "../../../helpers/permissionHelper"; +import RolePermissionService from "../../../service/management/rolePermissionService"; +import { PermissionString } from "../../../type/permissionTypes"; +import DatabaseActionException from "../../../exceptions/databaseActionException"; + +export default abstract class RolePermissionCommandHandler { + /** + * @description update role permissions + * @param {UpdateRolePermissionsCommand} updateRolePermissions + * @returns {Promise} + */ + static async updatePermissions(updateRolePermissions: UpdateRolePermissionsCommand): Promise { + let currentPermissions = (await RolePermissionService.getByRole(updateRolePermissions.roleId)).map( + (r) => r.permission + ); + return await dataSource.manager + .transaction(async (manager) => { + let newPermissions = PermissionHelper.getWhatToAdd(currentPermissions, updateRolePermissions.permissions); + let removePermissions = PermissionHelper.getWhatToRemove(currentPermissions, updateRolePermissions.permissions); + if (newPermissions.length != 0) { + await this.updatePermissionsAdd(manager, updateRolePermissions.roleId, newPermissions); + } + if (removePermissions.length != 0) { + await this.updatePermissionsRemove(manager, updateRolePermissions.roleId, removePermissions); + } + }) + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("UPDATE", "rolePermissions", err); + }); + } + + private static async updatePermissionsAdd( + manager: EntityManager, + roleId: number, + permissions: Array + ): Promise { + return await manager + .createQueryBuilder() + .insert() + .into(rolePermission) + .values( + permissions.map((p) => ({ + permission: p, + roleId: roleId, + })) + ) + .orIgnore() + .execute(); + } + + private static async updatePermissionsRemove( + manager: EntityManager, + roleId: number, + permissions: Array + ): Promise { + return await manager + .createQueryBuilder() + .delete() + .from(rolePermission) + .where("roleId = :id", { id: roleId }) + .andWhere("permission IN (:...permission)", { permission: permissions }) + .execute(); + } +} diff --git a/src/command/management/user/inviteCommand.ts b/src/command/management/user/inviteCommand.ts new file mode 100644 index 0000000..4011c44 --- /dev/null +++ b/src/command/management/user/inviteCommand.ts @@ -0,0 +1,12 @@ +export interface CreateInviteCommand { + mail: string; + username: string; + firstname: string; + lastname: string; + secret: string; +} + +export interface DeleteInviteCommand { + token: string; + mail: string; +} diff --git a/src/command/management/user/inviteCommandHandler.ts b/src/command/management/user/inviteCommandHandler.ts new file mode 100644 index 0000000..5c01044 --- /dev/null +++ b/src/command/management/user/inviteCommandHandler.ts @@ -0,0 +1,75 @@ +import { dataSource } from "../../../data-source"; +import { invite } from "../../../entity/management/invite"; +import DatabaseActionException from "../../../exceptions/databaseActionException"; +import InternalException from "../../../exceptions/internalException"; +import { StringHelper } from "../../../helpers/stringHelper"; +import { CreateInviteCommand, DeleteInviteCommand } from "./inviteCommand"; + +export default abstract class InviteCommandHandler { + /** + * @description create user + * @param CreateInviteCommand + * @returns {Promise} + */ + static async create(createInvite: CreateInviteCommand): Promise { + const token = StringHelper.random(32); + + return await dataSource + .createQueryBuilder() + .insert() + .into(invite) + .values({ + mail: createInvite.mail, + token: token, + username: createInvite.username, + firstname: createInvite.firstname, + lastname: createInvite.lastname, + secret: createInvite.secret, + }) + .orUpdate(["firstName", "lastName", "token", "secret"], ["mail"]) + .execute() + .then((result) => { + return token; + }) + .catch((err) => { + throw new DatabaseActionException("CREATE", "invite", err); + }); + } + + /** + * @description delete invite by mail and token + * @param DeleteInviteCommand + * @returns {Promise} + */ + static async deleteByTokenAndMail(deleteInvite: DeleteInviteCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(invite) + .where("invite.token = :token", { token: deleteInvite.token }) + .andWhere("invite.mail = :mail", { mail: deleteInvite.mail }) + .execute() + .then((res) => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "invite", err); + }); + } + + /** + * @description delete invite by mail + * @param DeleteByMailInviteCommand + * @returns {Promise} + */ + static async deleteByMail(mail: string): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(invite) + .where("invite.mail = :mail", { mail }) + .execute() + .then((res) => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "invite", err); + }); + } +} diff --git a/src/command/management/user/userCommand.ts b/src/command/management/user/userCommand.ts new file mode 100644 index 0000000..90f9872 --- /dev/null +++ b/src/command/management/user/userCommand.ts @@ -0,0 +1,35 @@ +export interface CreateUserCommand { + mail: string; + username: string; + firstname: string; + lastname: string; + secret: string; + isOwner: boolean; +} + +export interface UpdateUserCommand { + id: string; + mail: string; + username: string; + firstname: string; + lastname: string; +} + +export interface UpdateUserSecretCommand { + id: string; + secret: string; +} + +export interface TransferUserOwnerCommand { + fromId: string; + toId: string; +} + +export interface UpdateUserRolesCommand { + id: string; + roleIds: Array; +} + +export interface DeleteUserCommand { + id: string; +} diff --git a/src/command/management/user/userCommandHandler.ts b/src/command/management/user/userCommandHandler.ts new file mode 100644 index 0000000..590b2de --- /dev/null +++ b/src/command/management/user/userCommandHandler.ts @@ -0,0 +1,170 @@ +import { EntityManager } from "typeorm"; +import { dataSource } from "../../../data-source"; +import { user } from "../../../entity/management/user"; +import InternalException from "../../../exceptions/internalException"; +import { + CreateUserCommand, + DeleteUserCommand, + TransferUserOwnerCommand, + UpdateUserCommand, + UpdateUserRolesCommand, + UpdateUserSecretCommand, +} from "./userCommand"; +import UserService from "../../../service/management/userService"; +import DatabaseActionException from "../../../exceptions/databaseActionException"; + +export default abstract class UserCommandHandler { + /** + * @description create user + * @param {CreateUserCommand} createUser + * @returns {Promise} + */ + static async create(createUser: CreateUserCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(user) + .values({ + username: createUser.username, + mail: createUser.mail, + firstname: createUser.firstname, + lastname: createUser.lastname, + secret: createUser.secret, + isOwner: createUser.isOwner, + }) + .execute() + .then((result) => { + return result.identifiers[0].id; + }) + .catch((err) => { + throw new DatabaseActionException("CREATE", "user", err); + }); + } + + /** + * @description update user + * @param {UpdateUserCommand} updateUser + * @returns {Promise} + */ + static async update(updateUser: UpdateUserCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(user) + .set({ + mail: updateUser.mail, + firstname: updateUser.firstname, + lastname: updateUser.lastname, + username: updateUser.username, + }) + .where("id = :id", { id: updateUser.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("UPDATE", "user", err); + }); + } + + /** + * @description update user + * @param {UpdateUserSecretCommand} updateUser + * @returns {Promise} + */ + static async updateSecret(updateUser: UpdateUserSecretCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(user) + .set({ + secret: updateUser.secret, + }) + .where("id = :id", { id: updateUser.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("UPDATE", "user", err); + }); + } + + /** + * @description update user roles + * @param {UpdateUserRolesCommand} updateUserRoles + * @returns {Promise} + */ + static async updateRoles(updateUserRoles: UpdateUserRolesCommand): Promise { + let currentRoles = (await UserService.getAssignedRolesByUserId(updateUserRoles.id)).map((r) => r.id); + return await dataSource.manager + .transaction(async (manager) => { + let newRoles = updateUserRoles.roleIds.filter((r) => !currentRoles.includes(r)); + let removeRoles = currentRoles.filter((r) => !updateUserRoles.roleIds.includes(r)); + + for (let role of newRoles) { + await this.updateRolesAdd(manager, updateUserRoles.id, role); + } + + for (let role of removeRoles) { + await this.updateRolesRemove(manager, updateUserRoles.id, role); + } + }) + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("UPDATE", "userRoles", err); + }); + } + + private static async updateRolesAdd(manager: EntityManager, userId: string, roleId: number): Promise { + return await manager.createQueryBuilder().relation(user, "roles").of(userId).add(roleId); + } + + private static async updateRolesRemove(manager: EntityManager, userId: string, roleId: number): Promise { + return await manager.createQueryBuilder().relation(user, "roles").of(userId).remove(roleId); + } + + /** + * @description transfer ownership + * @param {TransferUserOwnerCommand} transferOwnership + * @returns {Promise} + */ + static async transferOwnership(transferOwnership: TransferUserOwnerCommand): Promise { + return await dataSource.manager + .transaction(async (manager) => { + await manager + .createQueryBuilder() + .update(user) + .set({ + isOwner: false, + }) + .where("id = :id", { id: transferOwnership.fromId }) + .execute(); + + await manager + .createQueryBuilder() + .update(user) + .set({ + isOwner: true, + }) + .where("id = :id", { id: transferOwnership.toId }) + .execute(); + }) + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("ABORT", "transfer owner", err); + }); + } + + /** + * @description delete user + * @param DeleteUserCommand + * @returns {Promise} + */ + static async delete(deleteUser: DeleteUserCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(user) + .where("id = :id", { id: deleteUser.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "user", err); + }); + } +} diff --git a/src/command/management/user/userPermissionCommand.ts b/src/command/management/user/userPermissionCommand.ts new file mode 100644 index 0000000..c7a8ae6 --- /dev/null +++ b/src/command/management/user/userPermissionCommand.ts @@ -0,0 +1,16 @@ +import { PermissionString } from "../../../type/permissionTypes"; + +export interface CreateUserPermissionCommand { + permission: PermissionString; + userId: string; +} + +export interface DeleteUserPermissionCommand { + permission: PermissionString; + userId: string; +} + +export interface UpdateUserPermissionsCommand { + userId: string; + permissions: Array; +} diff --git a/src/command/management/user/userPermissionCommandHandler.ts b/src/command/management/user/userPermissionCommandHandler.ts new file mode 100644 index 0000000..337f68a --- /dev/null +++ b/src/command/management/user/userPermissionCommandHandler.ts @@ -0,0 +1,76 @@ +import { DeleteResult, EntityManager, InsertResult } from "typeorm"; +import { dataSource } from "../../../data-source"; +import { user } from "../../../entity/management/user"; +import { userPermission } from "../../../entity/management/user_permission"; +import InternalException from "../../../exceptions/internalException"; +import { + CreateUserPermissionCommand, + DeleteUserPermissionCommand, + UpdateUserPermissionsCommand, +} from "./userPermissionCommand"; +import UserPermissionService from "../../../service/management/userPermissionService"; +import PermissionHelper from "../../../helpers/permissionHelper"; +import { PermissionString } from "../../../type/permissionTypes"; +import DatabaseActionException from "../../../exceptions/databaseActionException"; + +export default abstract class UserPermissionCommandHandler { + /** + * @description update user permissions + * @param {UpdateUserPermissionsCommand} updateUserPermissions + * @returns {Promise} + */ + static async updatePermissions(updateUserPermissions: UpdateUserPermissionsCommand): Promise { + let currentPermissions = (await UserPermissionService.getByUser(updateUserPermissions.userId)).map( + (r) => r.permission + ); + return await dataSource.manager + .transaction(async (manager) => { + let newPermissions = PermissionHelper.getWhatToAdd(currentPermissions, updateUserPermissions.permissions); + let removePermissions = PermissionHelper.getWhatToRemove(currentPermissions, updateUserPermissions.permissions); + + if (newPermissions.length != 0) { + await this.updatePermissionsAdd(manager, updateUserPermissions.userId, newPermissions); + } + if (removePermissions.length != 0) { + await this.updatePermissionsRemove(manager, updateUserPermissions.userId, removePermissions); + } + }) + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("UPDATE", "userPermissions", err); + }); + } + + private static async updatePermissionsAdd( + manager: EntityManager, + userId: string, + permissions: Array + ): Promise { + return await manager + .createQueryBuilder() + .insert() + .into(userPermission) + .values( + permissions.map((p) => ({ + permission: p, + userId: userId, + })) + ) + .orIgnore() + .execute(); + } + + private static async updatePermissionsRemove( + manager: EntityManager, + userId: string, + permissions: Array + ): Promise { + return await manager + .createQueryBuilder() + .delete() + .from(userPermission) + .where("userId = :id", { id: userId }) + .andWhere("permission IN (:...permission)", { permission: permissions }) + .execute(); + } +} diff --git a/src/command/refreshCommand.ts b/src/command/refreshCommand.ts new file mode 100644 index 0000000..0aa0896 --- /dev/null +++ b/src/command/refreshCommand.ts @@ -0,0 +1,9 @@ +export interface CreateRefreshCommand { + userId: string; + isFromPwa?: boolean; +} + +export interface DeleteRefreshCommand { + token: string; + userId: string; +} diff --git a/src/command/refreshCommandHandler.ts b/src/command/refreshCommandHandler.ts new file mode 100644 index 0000000..df6a8ea --- /dev/null +++ b/src/command/refreshCommandHandler.ts @@ -0,0 +1,74 @@ +import { dataSource } from "../data-source"; +import { refresh } from "../entity/refresh"; +import { PWA_REFRESH_EXPIRATION, REFRESH_EXPIRATION } from "../env.defaults"; +import DatabaseActionException from "../exceptions/databaseActionException"; +import InternalException from "../exceptions/internalException"; +import { StringHelper } from "../helpers/stringHelper"; +import UserService from "../service/management/userService"; +import { CreateRefreshCommand, DeleteRefreshCommand } from "./refreshCommand"; +import ms from "ms"; + +export default abstract class RefreshCommandHandler { + /** + * @description create and save refreshToken to user + * @param {CreateRefreshCommand} createRefresh + * @returns {Promise} + */ + static async create(createRefresh: CreateRefreshCommand): Promise { + const refreshToken = StringHelper.random(32); + + return await dataSource + .createQueryBuilder() + .insert() + .into(refresh) + .values({ + token: refreshToken, + userId: createRefresh.userId, + expiry: createRefresh.isFromPwa + ? new Date(Date.now() + ms(PWA_REFRESH_EXPIRATION)) + : new Date(Date.now() + ms(REFRESH_EXPIRATION)), + }) + .execute() + .then((result) => { + return refreshToken; + }) + .catch((err) => { + throw new DatabaseActionException("CREATE", "refresh", err); + }); + } + + /** + * @description delete refresh by user and token + * @param {DeleteRefreshCommand} deleteRefresh + * @returns {Promise} + */ + static async deleteByToken(deleteRefresh: DeleteRefreshCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(refresh) + .where({ token: deleteRefresh.token, userId: deleteRefresh.userId }) + .execute() + .then((res) => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "refresh", err); + }); + } + + /** + * @description delete expired + * @returns {Promise} + */ + static async deleteExpired(): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(refresh) + .where("refresh.expiry < :expiry", { expiry: new Date() }) + .execute() + .then((res) => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "refresh", err); + }); + } +} diff --git a/src/command/resetCommand.ts b/src/command/resetCommand.ts new file mode 100644 index 0000000..0382771 --- /dev/null +++ b/src/command/resetCommand.ts @@ -0,0 +1,10 @@ +export interface CreateResetCommand { + mail: string; + username: string; + secret: string; +} + +export interface DeleteResetCommand { + token: string; + mail: string; +} diff --git a/src/command/resetCommandHandler.ts b/src/command/resetCommandHandler.ts new file mode 100644 index 0000000..8c7d06e --- /dev/null +++ b/src/command/resetCommandHandler.ts @@ -0,0 +1,55 @@ +import { dataSource } from "../data-source"; +import { reset } from "../entity/reset"; +import DatabaseActionException from "../exceptions/databaseActionException"; +import InternalException from "../exceptions/internalException"; +import { StringHelper } from "../helpers/stringHelper"; +import { CreateResetCommand, DeleteResetCommand } from "./resetCommand"; + +export default abstract class ResetCommandHandler { + /** + * @description create user + * @param {CreateResetCommand} createReset + * @returns {Promise} + */ + static async create(createReset: CreateResetCommand): Promise { + const token = StringHelper.random(32); + + return await dataSource + .createQueryBuilder() + .insert() + .into(reset) + .values({ + token: token, + mail: createReset.mail, + username: createReset.username, + secret: createReset.secret, + }) + .orUpdate(["token", "secret"], ["mail"]) + .execute() + .then((result) => { + return token; + }) + .catch((err) => { + throw new DatabaseActionException("CREATE", "reset", err); + }); + } + + /** + * @description delete reset by mail and token + * @param {DeleteRefreshCommand} deleteReset + * @returns {Promise} + */ + static async deleteByTokenAndMail(deleteReset: DeleteResetCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(reset) + .where("reset.token = :token", { token: deleteReset.token }) + .andWhere("reset.mail = :mail", { mail: deleteReset.mail }) + .execute() + .then((res) => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "reset", err); + }); + } +} diff --git a/src/controller/admin/configuration/memberController.ts b/src/controller/admin/configuration/memberController.ts new file mode 100644 index 0000000..8225c98 --- /dev/null +++ b/src/controller/admin/configuration/memberController.ts @@ -0,0 +1,137 @@ +import { Request, Response } from "express"; +import MemberService from "../../../service/configuration/memberService"; +import MemberFactory from "../../../factory/admin/configuration/member"; +import { + CreateMemberCommand, + DeleteMemberCommand, + UpdateMemberCommand, +} from "../../../command/configuration/member/memberCommand"; +import MemberCommandHandler from "../../../command/configuration/member/memberCommandHandler"; + +/** + * @description get all members + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllMembers(req: Request, res: Response): Promise { + let offset = parseInt((req.query.offset as string) ?? "0"); + let count = parseInt((req.query.count as string) ?? "25"); + let search = (req.query.search as string) ?? ""; + let noLimit = req.query.noLimit === "true"; + let ids = ((req.query.ids ?? "") as string).split(",").filter((i) => i); + + let [members, total] = await MemberService.getAll({ offset, count, search, noLimit, ids }); + + res.json({ + members: MemberFactory.mapToBase(members), + total: total, + offset: offset, + count: count, + }); +} + +/** + * @description get members by Ids + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getMembersByIds(req: Request, res: Response): Promise { + let ids = req.body.ids as Array; + + let [members, total] = await MemberService.getAll({ noLimit: true, ids }); + + res.json({ + members: MemberFactory.mapToBase(members), + total: total, + offset: 0, + count: total, + }); +} + +/** + * @description get member by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getMemberById(req: Request, res: Response): Promise { + const memberId = req.params.id; + let member = await MemberService.getById(memberId); + + res.json(MemberFactory.mapToSingle(member)); +} + +/** + * @description create member + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createMember(req: Request, res: Response): Promise { + const salutationId = parseInt(req.body.salutationId); + const firstname = req.body.firstname; + const lastname = req.body.lastname; + const nameaffix = req.body.nameaffix; + const birthdate = req.body.birthdate; + const internalId = req.body.internalId || null; + + let createMember: CreateMemberCommand = { + salutationId, + firstname, + lastname, + nameaffix, + birthdate, + internalId, + }; + let memberId = await MemberCommandHandler.create(createMember); + + res.status(200).send(memberId); +} + +/** + * @description update member by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateMemberById(req: Request, res: Response): Promise { + const memberId = req.params.id; + const salutationId = parseInt(req.body.salutationId); + const firstname = req.body.firstname; + const lastname = req.body.lastname; + const nameaffix = req.body.nameaffix; + const birthdate = req.body.birthdate; + const internalId = req.body.internalId || null; + + let updateMember: UpdateMemberCommand = { + id: memberId, + salutationId, + firstname, + lastname, + nameaffix, + birthdate, + internalId, + }; + await MemberCommandHandler.update(updateMember); + + res.sendStatus(204); +} + +/** + * @description delete member by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function deleteMemberById(req: Request, res: Response): Promise { + const memberId = req.params.id; + + let deleteMember: DeleteMemberCommand = { + id: memberId, + }; + await MemberCommandHandler.delete(deleteMember); + + res.sendStatus(204); +} diff --git a/src/controller/admin/management/backupController.ts b/src/controller/admin/management/backupController.ts new file mode 100644 index 0000000..25ef6dd --- /dev/null +++ b/src/controller/admin/management/backupController.ts @@ -0,0 +1,126 @@ +import { Request, Response } from "express"; +import { FileSystemHelper } from "../../../helpers/fileSystemHelper"; +import BackupHelper from "../../../helpers/backupHelper"; +import InternalException from "../../../exceptions/internalException"; + +/** + * @description get generated backups + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getGeneratedBackups(req: Request, res: Response): Promise { + let filesInFolder = FileSystemHelper.getFilesInDirectory(`backup`); + + let sorted = filesInFolder.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime()); + + res.json(sorted); +} + +/** + * @description download backup file + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function downloadBackupFile(req: Request, res: Response): Promise { + let filename = req.params.filename; + + let filepath = FileSystemHelper.formatPath("backup", filename); + + res.sendFile(filepath, { + headers: { + "Content-Type": "application/json", + }, + }); +} + +/** + * @description get uploaded backups + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getUploadedBackups(req: Request, res: Response): Promise { + let filesInFolder = FileSystemHelper.getFilesInDirectory("uploaded-backup"); + + let sorted = filesInFolder.sort((a, b) => new Date(b.split("_")[0]).getTime() - new Date(a.split("_")[0]).getTime()); + + res.json(sorted); +} + +/** + * @description download uploaded backup file + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function downloadUploadedBackupFile(req: Request, res: Response): Promise { + let filename = req.params.filename; + + let filepath = FileSystemHelper.formatPath("uploaded-backup", filename); + + res.sendFile(filepath, { + headers: { + "Content-Type": "application/json", + }, + }); +} + +/** + * @description create backup manually + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createManualBackup(req: Request, res: Response): Promise { + await BackupHelper.createBackup({}); + + res.sendStatus(204); +} + +/** + * @description restore backup by selected + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function restoreBackupByLocalFile(req: Request, res: Response): Promise { + let filename = req.body.filename; + let partial = req.body.partial; + let include = req.body.include; + let overwrite = req.body.overwrite; + + await BackupHelper.loadBackup({ filename, include, partial, overwrite }); + + res.sendStatus(204); +} + +/** + * @description restore uploaded backup by selected + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function restoreBackupByUploadedFile(req: Request, res: Response): Promise { + let filename = req.body.filename; + let partial = req.body.partial; + let include = req.body.include; + + await BackupHelper.loadBackup({ filename, path: "uploaded-backup", include, partial }); + + res.sendStatus(204); +} + +/** + * @description upload backup + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function uploadBackupFile(req: Request, res: Response): Promise { + if (!req.file) { + throw new InternalException("File upload failed"); + } + res.sendStatus(204); +} diff --git a/src/controller/admin/management/roleController.ts b/src/controller/admin/management/roleController.ts new file mode 100644 index 0000000..3af6ec8 --- /dev/null +++ b/src/controller/admin/management/roleController.ts @@ -0,0 +1,121 @@ +import { Request, Response } from "express"; +import RoleService from "../../../service/management/roleService"; +import RoleFactory from "../../../factory/admin/management/role"; +import RolePermissionService from "../../../service/management/rolePermissionService"; +import PermissionHelper from "../../../helpers/permissionHelper"; +import { CreateRoleCommand, DeleteRoleCommand, UpdateRoleCommand } from "../../../command/management/role/roleCommand"; +import RoleCommandHandler from "../../../command/management/role/roleCommandHandler"; +import { UpdateRolePermissionsCommand } from "../../../command/management/role/rolePermissionCommand"; +import RolePermissionCommandHandler from "../../../command/management/role/rolePermissionCommandHandler"; + +/** + * @description get All roles + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllRoles(req: Request, res: Response): Promise { + let roles = await RoleService.getAll(); + + res.json(RoleFactory.mapToBase(roles)); +} + +/** + * @description get role by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getRoleById(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + let role = await RoleService.getById(id); + + res.json(RoleFactory.mapToSingle(role)); +} + +/** + * @description get permissions by role + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getRolePermissions(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + let permissions = await RolePermissionService.getByRole(id); + + res.json(PermissionHelper.convertToObject(permissions.map((p) => p.permission))); +} + +/** + * @description create new role + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createRole(req: Request, res: Response): Promise { + let role = req.body.role; + + let createRole: CreateRoleCommand = { + role: role, + }; + await RoleCommandHandler.create(createRole); + + res.sendStatus(204); +} + +/** + * @description update role data + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateRole(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + let role = req.body.role; + + let updateRole: UpdateRoleCommand = { + id: id, + role: role, + }; + await RoleCommandHandler.update(updateRole); + + res.sendStatus(204); +} + +/** + * @description update role assigned permission strings + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateRolePermissions(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + let permissions = req.body.permissions; + + let permissionStrings = PermissionHelper.convertToStringArray(permissions); + + let updateRolePermissions: UpdateRolePermissionsCommand = { + roleId: id, + permissions: permissionStrings, + }; + await RolePermissionCommandHandler.updatePermissions(updateRolePermissions); + + res.sendStatus(204); +} + +/** + * @description delete role by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function deleteRole(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + + let deleteRole: DeleteRoleCommand = { + id: id, + }; + await RoleCommandHandler.delete(deleteRole); + + res.sendStatus(204); +} diff --git a/src/controller/admin/management/userController.ts b/src/controller/admin/management/userController.ts new file mode 100644 index 0000000..ba2a134 --- /dev/null +++ b/src/controller/admin/management/userController.ts @@ -0,0 +1,166 @@ +import { Request, Response } from "express"; +import UserService from "../../../service/management/userService"; +import UserFactory from "../../../factory/admin/management/user"; +import UserPermissionService from "../../../service/management/userPermissionService"; +import PermissionHelper from "../../../helpers/permissionHelper"; +import RoleFactory from "../../../factory/admin/management/role"; +import { + DeleteUserCommand, + UpdateUserCommand, + UpdateUserRolesCommand, +} from "../../../command/management/user/userCommand"; +import UserCommandHandler from "../../../command/management/user/userCommandHandler"; +import MailHelper from "../../../helpers/mailHelper"; +import { CLUB_NAME } from "../../../env.defaults"; +import { UpdateUserPermissionsCommand } from "../../../command/management/user/userPermissionCommand"; +import UserPermissionCommandHandler from "../../../command/management/user/userPermissionCommandHandler"; +import BadRequestException from "../../../exceptions/badRequestException"; + +/** + * @description get All users + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllUsers(req: Request, res: Response): Promise { + let users = await UserService.getAll(); + + res.json(UserFactory.mapToBase(users)); +} + +/** + * @description get user by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getUserById(req: Request, res: Response): Promise { + const id = req.params.id; + let user = await UserService.getById(id); + + res.json(UserFactory.mapToSingle(user)); +} + +/** + * @description get permissions by user + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getUserPermissions(req: Request, res: Response): Promise { + const id = req.params.id; + let permissions = await UserPermissionService.getByUser(id); + + res.json(PermissionHelper.convertToObject(permissions.map((p) => p.permission))); +} + +/** + * @description get assigned roles by user + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getUserRoles(req: Request, res: Response): Promise { + const id = req.params.id; + + let roles = await UserService.getAssignedRolesByUserId(id); + + res.json(RoleFactory.mapToBase(roles)); +} + +/** + * @description update user data + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateUser(req: Request, res: Response): Promise { + const id = req.params.id; + let mail = req.body.mail; + let firstname = req.body.firstname; + let lastname = req.body.lastname; + let username = req.body.username; + + let updateUser: UpdateUserCommand = { + id: id, + mail: mail, + firstname: firstname, + lastname: lastname, + username: username, + }; + await UserCommandHandler.update(updateUser); + + res.sendStatus(204); +} + +/** + * @description update user assigned permission strings + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateUserPermissions(req: Request, res: Response): Promise { + const id = req.params.id; + let permissions = req.body.permissions; + + let permissionStrings = PermissionHelper.convertToStringArray(permissions); + + let updateUserPermissions: UpdateUserPermissionsCommand = { + userId: id, + permissions: permissionStrings, + }; + await UserPermissionCommandHandler.updatePermissions(updateUserPermissions); + + res.sendStatus(204); +} + +/** + * @description update user assigned roles + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateUserRoles(req: Request, res: Response): Promise { + const id = req.params.id; + let roleIds = req.body.roleIds as Array; + + let updateRoles: UpdateUserRolesCommand = { + id: id, + roleIds: roleIds, + }; + await UserCommandHandler.updateRoles(updateRoles); + + res.sendStatus(204); +} + +/** + * @description delete user by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function deleteUser(req: Request, res: Response): Promise { + const id = req.params.id; + + let { mail, isOwner } = await UserService.getById(id); + + if (isOwner) { + throw new BadRequestException("Owner cannot be deleted"); + } + + let deleteUser: DeleteUserCommand = { + id: id, + }; + await UserCommandHandler.delete(deleteUser); + + try { + // sendmail + await MailHelper.sendMail( + mail, + `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, + `Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.` + ); + } catch (error) {} + + res.sendStatus(204); +} diff --git a/src/controller/authController.ts b/src/controller/authController.ts new file mode 100644 index 0000000..9ecfa64 --- /dev/null +++ b/src/controller/authController.ts @@ -0,0 +1,98 @@ +import { Request, Response } from "express"; +import { JWTHelper } from "../helpers/jwtHelper"; +import { JWTToken } from "../type/jwtTypes"; +import InternalException from "../exceptions/internalException"; +import RefreshCommandHandler from "../command/refreshCommandHandler"; +import { CreateRefreshCommand, DeleteRefreshCommand } from "../command/refreshCommand"; +import UserService from "../service/management/userService"; +import speakeasy from "speakeasy"; +import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; +import RefreshService from "../service/refreshService"; + +/** + * @description Check authentication status by token + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function login(req: Request, res: Response): Promise { + let username = req.body.username; + let totp = req.body.totp; + + let { id, secret } = await UserService.getByUsername(username); + + let valid = speakeasy.totp.verify({ + secret: secret, + encoding: "base32", + token: totp, + window: 2, + }); + + if (!valid) { + throw new UnauthorizedRequestException("Token not valid or expired"); + } + + let accessToken = await JWTHelper.buildToken(id); + + let refreshCommand: CreateRefreshCommand = { + userId: id, + isFromPwa: req.isPWA, + }; + let refreshToken = await RefreshCommandHandler.create(refreshCommand); + + res.json({ + accessToken, + refreshToken, + }); +} + +/** + * @description logout user by token (invalidate refresh token) + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function logout(req: Request, res: Response): Promise {} + +/** + * @description refresh expired token + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function refresh(req: Request, res: Response): Promise { + let token = req.body.accessToken; + let refresh = req.body.refreshToken; + + const tokenUser = await JWTHelper.decode(token); + if (typeof tokenUser == "string" || !tokenUser) { + throw new InternalException("process failed"); + } + + let tokenUserId = (tokenUser as JWTToken).userId; + + let { user } = await RefreshService.getByToken(refresh); + + if (tokenUserId != user.id) { + throw new UnauthorizedRequestException("user not identified with token and refresh"); + } + + let accessToken = await JWTHelper.buildToken(tokenUserId); + + let refreshCommand: CreateRefreshCommand = { + userId: tokenUserId, + isFromPwa: req.isPWA, + }; + let refreshToken = await RefreshCommandHandler.create(refreshCommand); + + let removeToken: DeleteRefreshCommand = { + userId: tokenUserId, + token: refresh, + }; + await RefreshCommandHandler.deleteByToken(removeToken); + + res.json({ + accessToken, + refreshToken, + }); +} diff --git a/src/controller/inviteController.ts b/src/controller/inviteController.ts new file mode 100644 index 0000000..0caebb8 --- /dev/null +++ b/src/controller/inviteController.ts @@ -0,0 +1,175 @@ +import { Request, Response } from "express"; +import { JWTHelper } from "../helpers/jwtHelper"; +import { JWTToken } from "../type/jwtTypes"; +import InternalException from "../exceptions/internalException"; +import RefreshCommandHandler from "../command/refreshCommandHandler"; +import { CreateRefreshCommand } from "../command/refreshCommand"; +import speakeasy from "speakeasy"; +import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; +import QRCode from "qrcode"; +import { CreateUserCommand } from "../command/management/user/userCommand"; +import UserCommandHandler from "../command/management/user/userCommandHandler"; +import { CreateInviteCommand, DeleteInviteCommand } from "../command/management/user/inviteCommand"; +import InviteCommandHandler from "../command/management/user/inviteCommandHandler"; +import MailHelper from "../helpers/mailHelper"; +import InviteService from "../service/management/inviteService"; +import UserService from "../service/management/userService"; +import CustomRequestException from "../exceptions/customRequestException"; +import { CLUB_NAME } from "../env.defaults"; +import { CreateUserPermissionCommand } from "../command/management/user/userPermissionCommand"; +import UserPermissionCommandHandler from "../command/management/user/userPermissionCommandHandler"; +import InviteFactory from "../factory/admin/management/invite"; + +/** + * @description get all invites + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getInvites(req: Request, res: Response): Promise { + let invites = await InviteService.getAll(); + + res.json(InviteFactory.mapToBase(invites)); +} + +/** + * @description start first user + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function inviteUser(req: Request, res: Response, isInvite: boolean = true): Promise { + let origin = req.headers.origin; + let username = req.body.username; + let mail = req.body.mail; + let firstname = req.body.firstname; + let lastname = req.body.lastname; + + let users = await UserService.getByMailOrUsername(mail, username); + if (users.length == 1) { + // username or mail is used + if (users[0].username == username && users[0].mail == mail) { + throw new CustomRequestException(409, "Username and Mail are already in use"); + } else if (users[0].username == username) { + throw new CustomRequestException(409, "Username is already in use"); + } else { + throw new CustomRequestException(409, "Mail is already in use"); + } + } else if (users.length >= 2) { + throw new CustomRequestException(409, "Username and Mail are already in use"); + } + + var secret = speakeasy.generateSecret({ length: 20, name: `FF Operation ${CLUB_NAME}` }); + + let createInvite: CreateInviteCommand = { + username: username, + mail: mail, + firstname: firstname, + lastname: lastname, + secret: secret.base32, + }; + let token = await InviteCommandHandler.create(createInvite); + + // sendmail + await MailHelper.sendMail( + mail, + `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, + `Öffne folgenden Link: ${origin}/${isInvite ? "invite" : "setup"}/verify?mail=${mail}&token=${token}` + ); + + res.sendStatus(204); +} + +/** + * @description Create first user + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function verifyInvite(req: Request, res: Response): Promise { + let mail = req.body.mail; + let token = req.body.token; + + let { secret, username } = await InviteService.getByMailAndToken(mail, token); + + const url = `otpauth://totp/FF Operation ${CLUB_NAME}?secret=${secret}`; + + QRCode.toDataURL(url) + .then((result) => { + res.json({ + dataUrl: result, + otp: secret, + username, + }); + }) + .catch((err) => { + throw new InternalException("QRCode not created", err); + }); +} + +/** + * @description Create first user + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function finishInvite(req: Request, res: Response, grantAdmin: boolean = false): Promise { + let mail = req.body.mail; + let token = req.body.token; + let totp = req.body.totp; + + let { secret, username, firstname, lastname } = await InviteService.getByMailAndToken(mail, token); + + let valid = speakeasy.totp.verify({ + secret: secret, + encoding: "base32", + token: totp, + window: 2, + }); + + if (!valid) { + throw new UnauthorizedRequestException("Token not valid or expired"); + } + + let createUser: CreateUserCommand = { + username: username, + firstname: firstname, + lastname: lastname, + mail: mail, + secret: secret, + isOwner: grantAdmin, + }; + let id = await UserCommandHandler.create(createUser); + + let accessToken = await JWTHelper.buildToken(id); + + let refreshCommand: CreateRefreshCommand = { + userId: id, + }; + let refreshToken = await RefreshCommandHandler.create(refreshCommand); + + let deleteInvite: DeleteInviteCommand = { + mail: mail, + token: token, + }; + await InviteCommandHandler.deleteByTokenAndMail(deleteInvite); + + res.json({ + accessToken, + refreshToken, + }); +} + +/** + * @description delete invite by mail + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function deleteInvite(req: Request, res: Response): Promise { + const mail = req.params.mail; + + await InviteCommandHandler.deleteByMail(mail); + + res.sendStatus(204); +} diff --git a/src/controller/resetController.ts b/src/controller/resetController.ts new file mode 100644 index 0000000..11e9895 --- /dev/null +++ b/src/controller/resetController.ts @@ -0,0 +1,124 @@ +import { Request, Response } from "express"; +import { JWTHelper } from "../helpers/jwtHelper"; +import InternalException from "../exceptions/internalException"; +import RefreshCommandHandler from "../command/refreshCommandHandler"; +import { CreateRefreshCommand } from "../command/refreshCommand"; +import speakeasy from "speakeasy"; +import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; +import QRCode from "qrcode"; +import { CreateResetCommand, DeleteResetCommand } from "../command/resetCommand"; +import ResetCommandHandler from "../command/resetCommandHandler"; +import MailHelper from "../helpers/mailHelper"; +import ResetService from "../service/resetService"; +import UserService from "../service/management/userService"; +import { CLUB_NAME } from "../env.defaults"; +import { UpdateUserSecretCommand } from "../command/management/user/userCommand"; +import UserCommandHandler from "../command/management/user/userCommandHandler"; + +/** + * @description request totp reset + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function startReset(req: Request, res: Response): Promise { + let origin = req.headers.origin; + let username = req.body.username; + + let { mail } = await UserService.getByUsername(username); + + var secret = speakeasy.generateSecret({ length: 20, name: `FF Operation ${CLUB_NAME}` }); + + let createReset: CreateResetCommand = { + username: username, + mail: mail, + secret: secret.base32, + }; + let token = await ResetCommandHandler.create(createReset); + + // sendmail + await MailHelper.sendMail( + mail, + `Email Bestätigung für Einsatzverwaltung Admin-Portal von ${CLUB_NAME}`, + `Öffne folgenden Link: ${origin}/reset/reset?mail=${mail}&token=${token}` + ); + + res.sendStatus(204); +} + +/** + * @description verify reset link + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function verifyReset(req: Request, res: Response): Promise { + let mail = req.body.mail; + let token = req.body.token; + + let { secret } = await ResetService.getByMailAndToken(mail, token); + + const url = `otpauth://totp/FF Operation ${CLUB_NAME}?secret=${secret}`; + + QRCode.toDataURL(url) + .then((result) => { + res.json({ + dataUrl: result, + otp: secret, + }); + }) + .catch((err) => { + throw new InternalException("QRCode not created", err); + }); +} + +/** + * @description finishReset + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function finishReset(req: Request, res: Response): Promise { + let mail = req.body.mail; + let token = req.body.token; + let totp = req.body.totp; + + let { secret, username } = await ResetService.getByMailAndToken(mail, token); + + let valid = speakeasy.totp.verify({ + secret: secret, + encoding: "base32", + token: totp, + window: 2, + }); + + if (!valid) { + throw new UnauthorizedRequestException("Token not valid or expired"); + } + + let { id } = await UserService.getByUsername(username); + + let updateUserSecret: UpdateUserSecretCommand = { + id, + secret, + }; + await UserCommandHandler.updateSecret(updateUserSecret); + + let accessToken = await JWTHelper.buildToken(id); + + let refreshCommand: CreateRefreshCommand = { + userId: id, + }; + let refreshToken = await RefreshCommandHandler.create(refreshCommand); + + let deleteReset: DeleteResetCommand = { + mail: mail, + token: token, + }; + await ResetCommandHandler.deleteByTokenAndMail(deleteReset); + + res.json({ + accessToken, + refreshToken, + }); +} diff --git a/src/controller/setupController.ts b/src/controller/setupController.ts new file mode 100644 index 0000000..f71194f --- /dev/null +++ b/src/controller/setupController.ts @@ -0,0 +1,11 @@ +import { Request, Response } from "express"; + +/** + * @description Service is currently not configured + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function isSetup(req: Request, res: Response): Promise { + res.sendStatus(204); +} diff --git a/src/controller/userController.ts b/src/controller/userController.ts new file mode 100644 index 0000000..d6e5f1c --- /dev/null +++ b/src/controller/userController.ts @@ -0,0 +1,121 @@ +import { Request, Response } from "express"; +import speakeasy from "speakeasy"; +import QRCode from "qrcode"; +import InternalException from "../exceptions/internalException"; +import { CLUB_NAME } from "../env.defaults"; +import UserService from "../service/management/userService"; +import UserFactory from "../factory/admin/management/user"; +import { TransferUserOwnerCommand, UpdateUserCommand } from "../command/management/user/userCommand"; +import UserCommandHandler from "../command/management/user/userCommandHandler"; +import ForbiddenRequestException from "../exceptions/forbiddenRequestException"; + +/** + * @description get my by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getMeById(req: Request, res: Response): Promise { + const id = req.userId; + let user = await UserService.getById(id); + + res.json(UserFactory.mapToSingle(user)); +} + +/** + * @description get my totp + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getMyTotp(req: Request, res: Response): Promise { + const userId = req.userId; + + let { secret } = await UserService.getById(userId); + + const url = `otpauth://totp/FF Operation ${CLUB_NAME}?secret=${secret}`; + + QRCode.toDataURL(url) + .then((result) => { + res.json({ + dataUrl: result, + otp: secret, + }); + }) + .catch((err) => { + throw new InternalException("QRCode not created", err); + }); +} + +/** + * @description verify my totp + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function verifyMyTotp(req: Request, res: Response): Promise { + const userId = req.userId; + let totp = req.body.totp; + + let { secret } = await UserService.getById(userId); + let valid = speakeasy.totp.verify({ + secret: secret, + encoding: "base32", + token: totp, + window: 2, + }); + + if (!valid) { + throw new InternalException("Token not valid or expired"); + } + res.sendStatus(204); +} + +/** + * @description transferOwnership + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function transferOwnership(req: Request, res: Response): Promise { + const userId = req.userId; + let toId = req.body.toId; + + let { isOwner } = await UserService.getById(userId); + if (!isOwner) { + throw new ForbiddenRequestException("Action only allowed to owner."); + } + + let transfer: TransferUserOwnerCommand = { + toId: toId, + fromId: userId, + }; + await UserCommandHandler.transferOwnership(transfer); + + res.sendStatus(204); +} + +/** + * @description update my data + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateMe(req: Request, res: Response): Promise { + const id = req.userId; + let mail = req.body.mail; + let firstname = req.body.firstname; + let lastname = req.body.lastname; + let username = req.body.username; + + let updateUser: UpdateUserCommand = { + id: id, + mail: mail, + firstname: firstname, + lastname: lastname, + username: username, + }; + await UserCommandHandler.update(updateUser); + + res.sendStatus(204); +} diff --git a/src/data-source.ts b/src/data-source.ts new file mode 100644 index 0000000..3585264 --- /dev/null +++ b/src/data-source.ts @@ -0,0 +1,34 @@ +import "dotenv/config"; +import "reflect-metadata"; +import { DataSource } from "typeorm"; +import { DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME, DB_TYPE, DB_PORT } from "./env.defaults"; + +import { user } from "./entity/management/user"; +import { refresh } from "./entity/refresh"; +import { invite } from "./entity/management/invite"; +import { userPermission } from "./entity/management/user_permission"; +import { role } from "./entity/management/role"; +import { rolePermission } from "./entity/management/role_permission"; +import { member } from "./entity/configuration/member"; +import { reset } from "./entity/reset"; + +import { CreateSchema1739697068682 } from "./migrations/1739697068682-CreateSchema"; + +const dataSource = new DataSource({ + type: DB_TYPE as any, + host: DB_HOST, + port: DB_PORT, + username: DB_USERNAME, + password: DB_PASSWORD, + database: DB_NAME, + synchronize: false, + logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"], + bigNumberStrings: false, + entities: [user, refresh, invite, reset, userPermission, role, rolePermission, member], + migrations: [CreateSchema1739697068682], + migrationsRun: true, + migrationsTransactionMode: "each", + subscribers: [], +}); + +export { dataSource }; diff --git a/src/entity/configuration/member.ts b/src/entity/configuration/member.ts new file mode 100644 index 0000000..999d02a --- /dev/null +++ b/src/entity/configuration/member.ts @@ -0,0 +1,16 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity() +export class member { + @PrimaryColumn({ generated: "uuid", type: "varchar" }) + id: string; + + @Column({ type: "varchar", length: 255 }) + firstname: string; + + @Column({ type: "varchar", length: 255 }) + lastname: string; + + @Column({ type: "varchar", length: 255 }) + nameaffix: string; +} diff --git a/src/entity/management/invite.ts b/src/entity/management/invite.ts new file mode 100644 index 0000000..654bf74 --- /dev/null +++ b/src/entity/management/invite.ts @@ -0,0 +1,22 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity() +export class invite { + @PrimaryColumn({ type: "varchar", length: 255 }) + mail: string; + + @Column({ type: "varchar", length: 255 }) + token: string; + + @Column({ type: "varchar", length: 255 }) + username: string; + + @Column({ type: "varchar", length: 255 }) + firstname: string; + + @Column({ type: "varchar", length: 255 }) + lastname: string; + + @Column({ type: "varchar", length: 255 }) + secret: string; +} diff --git a/src/entity/management/role.ts b/src/entity/management/role.ts new file mode 100644 index 0000000..1cc76a9 --- /dev/null +++ b/src/entity/management/role.ts @@ -0,0 +1,22 @@ +import { Column, Entity, ManyToMany, OneToMany, PrimaryColumn } from "typeorm"; +import { user } from "./user"; +import { rolePermission } from "./role_permission"; + +@Entity() +export class role { + @PrimaryColumn({ generated: "increment", type: "int" }) + id: number; + + @Column({ type: "varchar", length: 255, unique: true }) + role: string; + + @ManyToMany(() => user, (user) => user.roles, { + nullable: false, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }) + users: user[]; + + @OneToMany(() => rolePermission, (rolePermission) => rolePermission.role, { cascade: ["insert"] }) + permissions: rolePermission[]; +} diff --git a/src/entity/management/role_permission.ts b/src/entity/management/role_permission.ts new file mode 100644 index 0000000..68d4331 --- /dev/null +++ b/src/entity/management/role_permission.ts @@ -0,0 +1,19 @@ +import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; +import { PermissionString } from "../../type/permissionTypes"; +import { role } from "./role"; + +@Entity() +export class rolePermission { + @PrimaryColumn({ type: "int" }) + roleId: number; + + @PrimaryColumn({ type: "varchar", length: 255 }) + permission: PermissionString; + + @ManyToOne(() => role, { + nullable: false, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }) + role: role; +} diff --git a/src/entity/management/user.ts b/src/entity/management/user.ts new file mode 100644 index 0000000..94ab3a3 --- /dev/null +++ b/src/entity/management/user.ts @@ -0,0 +1,44 @@ +import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from "typeorm"; +import { role } from "./role"; +import { userPermission } from "./user_permission"; + +@Entity() +export class user { + @PrimaryColumn({ generated: "uuid", type: "varchar" }) + id: string; + + @Column({ type: "varchar", unique: true, length: 255 }) + mail: string; + + @Column({ type: "varchar", unique: true, length: 255 }) + username: string; + + @Column({ type: "varchar", length: 255 }) + firstname: string; + + @Column({ type: "varchar", length: 255 }) + lastname: string; + + @Column({ type: "varchar", length: 255 }) + secret: string; + + @Column({ type: "boolean", default: false }) + static: boolean; + + @Column({ type: "boolean", default: false }) + isOwner: boolean; + + @ManyToMany(() => role, (role) => role.users, { + nullable: false, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + cascade: ["insert"], + }) + @JoinTable({ + name: "user_roles", + }) + roles: role[]; + + @OneToMany(() => userPermission, (userPermission) => userPermission.user, { cascade: ["insert"] }) + permissions: userPermission[]; +} diff --git a/src/entity/management/user_permission.ts b/src/entity/management/user_permission.ts new file mode 100644 index 0000000..1c6ccfd --- /dev/null +++ b/src/entity/management/user_permission.ts @@ -0,0 +1,19 @@ +import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; +import { user } from "./user"; +import { PermissionObject, PermissionString } from "../../type/permissionTypes"; + +@Entity() +export class userPermission { + @PrimaryColumn() + userId: string; + + @PrimaryColumn({ type: "varchar", length: 255 }) + permission: PermissionString; + + @ManyToOne(() => user, { + nullable: false, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }) + user: user; +} diff --git a/src/entity/management/webapi.ts b/src/entity/management/webapi.ts new file mode 100644 index 0000000..c7aad4b --- /dev/null +++ b/src/entity/management/webapi.ts @@ -0,0 +1,27 @@ +import { Column, ColumnType, CreateDateColumn, Entity, OneToMany, PrimaryColumn } from "typeorm"; +import { webapiPermission } from "./webapi_permission"; +import { getTypeByORM } from "../../migrations/ormHelper"; + +@Entity() +export class webapi { + @PrimaryColumn({ generated: "increment", type: "int" }) + id: number; + + @Column({ type: "text", unique: true, select: false }) + token: string; + + @Column({ type: "varchar", length: 255, unique: true }) + title: string; + + @CreateDateColumn() + createdAt: Date; + + @Column({ type: getTypeByORM("datetime").type as ColumnType, nullable: true }) + lastUsage?: Date; + + @Column({ type: getTypeByORM("date").type as ColumnType, nullable: true }) + expiry?: Date; + + @OneToMany(() => webapiPermission, (apiPermission) => apiPermission.webapi, { cascade: ["insert"] }) + permissions: webapiPermission[]; +} diff --git a/src/entity/management/webapi_permission.ts b/src/entity/management/webapi_permission.ts new file mode 100644 index 0000000..988c7b9 --- /dev/null +++ b/src/entity/management/webapi_permission.ts @@ -0,0 +1,19 @@ +import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm"; +import { PermissionObject, PermissionString } from "../../type/permissionTypes"; +import { webapi } from "./webapi"; + +@Entity() +export class webapiPermission { + @PrimaryColumn({ type: "int" }) + webapiId: number; + + @PrimaryColumn({ type: "varchar", length: 255 }) + permission: PermissionString; + + @ManyToOne(() => webapi, { + nullable: false, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }) + webapi: webapi; +} diff --git a/src/entity/refresh.ts b/src/entity/refresh.ts new file mode 100644 index 0000000..09f90a7 --- /dev/null +++ b/src/entity/refresh.ts @@ -0,0 +1,22 @@ +import { Column, ColumnType, Entity, ManyToOne, PrimaryColumn } from "typeorm"; +import { user } from "./management/user"; +import { getTypeByORM } from "../migrations/ormHelper"; + +@Entity() +export class refresh { + @PrimaryColumn({ type: "varchar", length: 255 }) + token: string; + + @PrimaryColumn() + userId: string; + + @Column({ type: getTypeByORM("datetime").type as ColumnType }) + expiry: Date; + + @ManyToOne(() => user, { + nullable: false, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }) + user: user; +} diff --git a/src/entity/reset.ts b/src/entity/reset.ts new file mode 100644 index 0000000..a7e2b92 --- /dev/null +++ b/src/entity/reset.ts @@ -0,0 +1,16 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity() +export class reset { + @PrimaryColumn({ type: "varchar", length: 255 }) + mail: string; + + @Column({ type: "varchar", length: 255 }) + token: string; + + @Column({ type: "varchar", length: 255 }) + username: string; + + @Column({ type: "varchar", length: 255 }) + secret: string; +} diff --git a/src/env.defaults.ts b/src/env.defaults.ts new file mode 100644 index 0000000..eed7d89 --- /dev/null +++ b/src/env.defaults.ts @@ -0,0 +1,116 @@ +import "dotenv/config"; +import ms from "ms"; +import ip from "ip"; + +export const DB_TYPE = process.env.DB_TYPE ?? "mysql"; +export const DB_HOST = process.env.DB_HOST ?? ""; +export const DB_PORT = Number(process.env.DB_PORT ?? 3306); +export const DB_NAME = process.env.DB_NAME ?? ""; +export const DB_USERNAME = process.env.DB_USERNAME ?? ""; +export const DB_PASSWORD = process.env.DB_PASSWORD ?? ""; + +export const SERVER_PORT = Number(process.env.SERVER_PORT ?? 5000); + +export const JWT_SECRET = process.env.JWT_SECRET ?? "my_jwt_secret_string_ilughfnadiuhgq§$IUZGFVRweiouarbt1oub3h5q4a"; +export const JWT_EXPIRATION = process.env.JWT_EXPIRATION ?? "15m"; +export const REFRESH_EXPIRATION = process.env.REFRESH_EXPIRATION ?? "1d"; +export const PWA_REFRESH_EXPIRATION = process.env.PWA_REFRESH_EXPIRATION ?? "5d"; + +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 SECURITY_STRICT_LIMIT_WINDOW = process.env.SECURITY_STRICT_LIMIT_WINDOW ?? "15m"; +export const SECURITY_STRICT_LIMIT_REQUEST_COUNT = Number(process.env.SECURITY_STRICT_LIMIT_REQUEST_COUNT ?? "15"); +export const USE_SECURITY_LIMIT = process.env.USE_SECURITY_LIMIT ?? "true"; +export const SECURITY_LIMIT_WINDOW = process.env.SECURITY_LIMIT_WINDOW ?? "1m"; +export const SECURITY_LIMIT_REQUEST_COUNT = Number(process.env.SECURITY_LIMIT_REQUEST_COUNT ?? "500"); + +export const TRUST_PROXY = ((): Array | string | boolean | number | null => { + const proxyVal = process.env.TRUST_PROXY; + if (!proxyVal) return null; + if (proxyVal == "true" || proxyVal == "false") { + return proxyVal == "true"; + } + if (!isNaN(Number(proxyVal))) { + return Number(proxyVal); + } + if (proxyVal.includes(",") && proxyVal.split(",").every((pv) => ip.isV4Format(pv) || ip.isV6Format(pv))) { + return proxyVal.split(","); + } + if (ip.isV4Format(proxyVal) || ip.isV6Format(proxyVal)) { + return proxyVal; + } + return null; +})(); + +export function configCheck() { + if (DB_TYPE != "mysql" && DB_TYPE != "sqlite" && DB_TYPE != "postgres") + throw new Error("set valid value to DB_TYPE (mysql|sqlite|postgres)"); + if ((DB_HOST == "" || typeof DB_HOST != "string") && DB_TYPE != "sqlite") + throw new Error("set valid value to DB_HOST"); + if (DB_NAME == "" || typeof DB_NAME != "string") throw new Error("set valid value to DB_NAME"); + if ((DB_USERNAME == "" || typeof DB_USERNAME != "string") && DB_TYPE != "sqlite") + throw new Error("set valid value to DB_USERNAME"); + if ((DB_PASSWORD == "" || typeof DB_PASSWORD != "string") && DB_TYPE != "sqlite") + throw new Error("set valid value to DB_PASSWORD"); + + if (isNaN(SERVER_PORT)) throw new Error("set valid numeric value to SERVER_PORT"); + + if (JWT_SECRET == "" || typeof JWT_SECRET != "string") throw new Error("set valid value to JWT_SECRET"); + checkMS(JWT_EXPIRATION, "JWT_EXPIRATION"); + checkMS(REFRESH_EXPIRATION, "REFRESH_EXPIRATION"); + checkMS(PWA_REFRESH_EXPIRATION, "PWA_REFRESH_EXPIRATION"); + + if (MAIL_USERNAME == "" || typeof MAIL_USERNAME != "string") throw new Error("set valid value to MAIL_USERNAME"); + if (MAIL_PASSWORD == "" || typeof MAIL_PASSWORD != "string") throw new Error("set valid value to MAIL_PASSWORD"); + if (MAIL_HOST == "" || typeof MAIL_HOST != "string") throw new Error("set valid value to MAIL_HOST"); + if (isNaN(MAIL_PORT)) throw new Error("set valid numeric value to MAIL_PORT"); + if (MAIL_SECURE != "true" && MAIL_SECURE != "false") throw new Error("set 'true' or 'false' to MAIL_SECURE"); + + if ( + CLUB_WEBSITE != "" && + !/^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test(CLUB_WEBSITE) + ) + throw new Error("CLUB_WEBSITE is not valid url"); + + if (BACKUP_AUTO_RESTORE != "true" && BACKUP_AUTO_RESTORE != "false") + throw new Error("set 'true' or 'false' to BACKUP_AUTO_RESTORE"); + if (BACKUP_INTERVAL < 1) throw new Error("BACKUP_INTERVAL has to be at least 1"); + if (BACKUP_COPIES < 1) throw new Error("BACKUP_COPIES has to be at least 1"); + + if (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false") + throw new Error("set 'true' or 'false' to USE_SECURITY_STRICT_LIMIT"); + checkMS(SECURITY_STRICT_LIMIT_WINDOW, "SECURITY_STRICT_LIMIT_WINDOW"); + if (isNaN(SECURITY_STRICT_LIMIT_REQUEST_COUNT)) + throw new Error("set valid numeric value to SECURITY_STRICT_LIMIT_REQUEST_COUNT"); + if (USE_SECURITY_LIMIT != "true" && USE_SECURITY_LIMIT != "false") + throw new Error("set 'true' or 'false' to USE_SECURITY_LIMIT"); + checkMS(SECURITY_LIMIT_WINDOW, "SECURITY_LIMIT_WINDOW"); + if (isNaN(SECURITY_LIMIT_REQUEST_COUNT)) throw new Error("set valid numeric value to SECURITY_LIMIT_REQUEST_COUNT"); + + if (!TRUST_PROXY && process.env.TRUST_PROXY) { + throw new Error("set valid boolean, number, ip or ips value to TRUST_PROXY"); + } +} + +function checkMS(input: string, origin: string) { + try { + const result = ms(input); + if (result === undefined) { + throw new Error(`set valid ms value to ${origin} -> [0-9]*(y|d|h|m|s)`); + } + } catch (e) { + throw new Error(`set valid ms value to ${origin} -> [0-9]*(y|d|h|m|s)`); + } +} diff --git a/src/exceptions/badRequestException.ts b/src/exceptions/badRequestException.ts new file mode 100644 index 0000000..fdb9a38 --- /dev/null +++ b/src/exceptions/badRequestException.ts @@ -0,0 +1,7 @@ +import CustomRequestException from "./customRequestException"; + +export default class BadRequestException extends CustomRequestException { + constructor(msg: string, err?: any) { + super(400, msg, err); + } +} diff --git a/src/exceptions/customRequestException.ts b/src/exceptions/customRequestException.ts new file mode 100644 index 0000000..133ba68 --- /dev/null +++ b/src/exceptions/customRequestException.ts @@ -0,0 +1,12 @@ +import { ExceptionBase } from "./exceptionsBaseType"; + +export default class CustomRequestException extends Error implements ExceptionBase { + statusCode: number; + err?: any; + + constructor(status: number, msg: string, err?: any) { + super(msg); + this.statusCode = status; + this.err = err; + } +} diff --git a/src/exceptions/databaseActionException.ts b/src/exceptions/databaseActionException.ts new file mode 100644 index 0000000..b0b145c --- /dev/null +++ b/src/exceptions/databaseActionException.ts @@ -0,0 +1,8 @@ +import CustomRequestException from "./customRequestException"; + +export default class DatabaseActionException extends CustomRequestException { + constructor(action: string, table: string, err: any) { + let errstring = `${action} on ${table} with ${err?.code ?? "XX"} at ${err?.sqlMessage ?? "XX"}`; + super(500, errstring, err); + } +} diff --git a/src/exceptions/exceptionsBaseType.ts b/src/exceptions/exceptionsBaseType.ts new file mode 100644 index 0000000..9eeb7fa --- /dev/null +++ b/src/exceptions/exceptionsBaseType.ts @@ -0,0 +1,3 @@ +export type ExceptionBase = { + statusCode: number; +} & Error; diff --git a/src/exceptions/forbiddenRequestException.ts b/src/exceptions/forbiddenRequestException.ts new file mode 100644 index 0000000..c2cb071 --- /dev/null +++ b/src/exceptions/forbiddenRequestException.ts @@ -0,0 +1,7 @@ +import CustomRequestException from "./customRequestException"; + +export default class ForbiddenRequestException extends CustomRequestException { + constructor(msg: string, err?: any) { + super(403, msg, err); + } +} diff --git a/src/exceptions/internalException.ts b/src/exceptions/internalException.ts new file mode 100644 index 0000000..342db4d --- /dev/null +++ b/src/exceptions/internalException.ts @@ -0,0 +1,7 @@ +import CustomRequestException from "./customRequestException"; + +export default class InternalException extends CustomRequestException { + constructor(msg: string, err?: any) { + super(500, msg, err); + } +} diff --git a/src/exceptions/unauthorizedRequestException.ts b/src/exceptions/unauthorizedRequestException.ts new file mode 100644 index 0000000..b1d273e --- /dev/null +++ b/src/exceptions/unauthorizedRequestException.ts @@ -0,0 +1,7 @@ +import CustomRequestException from "./customRequestException"; + +export default class UnauthorizedRequestException extends CustomRequestException { + constructor(msg: string, err?: any) { + super(401, msg, err); + } +} diff --git a/src/factory/admin/configuration/member.ts b/src/factory/admin/configuration/member.ts new file mode 100644 index 0000000..469af18 --- /dev/null +++ b/src/factory/admin/configuration/member.ts @@ -0,0 +1,27 @@ +import { member } from "../../../entity/configuration/member"; +import { MemberViewModel } from "../../../viewmodel/admin/configuration/member.models"; + +export default abstract class MemberFactory { + /** + * @description map record to member + * @param {member} record + * @returns {MemberViewModel} + */ + public static mapToSingle(record: member): MemberViewModel { + return { + id: record?.id, + firstname: record?.firstname, + lastname: record?.lastname, + nameaffix: record?.nameaffix, + }; + } + + /** + * @description map records to member + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/factory/admin/management/invite.ts b/src/factory/admin/management/invite.ts new file mode 100644 index 0000000..9ff0fc2 --- /dev/null +++ b/src/factory/admin/management/invite.ts @@ -0,0 +1,27 @@ +import { invite } from "../../../entity/management/invite"; +import { InviteViewModel } from "../../../viewmodel/admin/management/invite.models"; + +export default abstract class InviteFactory { + /** + * @description map record to invite + * @param {invite} record + * @returns {InviteViewModel} + */ + public static mapToSingle(record: invite): InviteViewModel { + return { + mail: record.mail, + username: record.username, + firstname: record.firstname, + lastname: record.lastname, + }; + } + + /** + * @description map records to invite + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/factory/admin/management/role.ts b/src/factory/admin/management/role.ts new file mode 100644 index 0000000..56bb479 --- /dev/null +++ b/src/factory/admin/management/role.ts @@ -0,0 +1,27 @@ +import { role } from "../../../entity/management/role"; +import PermissionHelper from "../../../helpers/permissionHelper"; +import { RoleViewModel } from "../../../viewmodel/admin/management/role.models"; + +export default abstract class RoleFactory { + /** + * @description map record to role + * @param {role} record + * @returns {roleViewModel} + */ + public static mapToSingle(record: role): RoleViewModel { + return { + id: record.id, + permissions: PermissionHelper.convertToObject(record.permissions.map((e) => e.permission)), + role: record.role, + }; + } + + /** + * @description map records to role + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/factory/admin/management/user.ts b/src/factory/admin/management/user.ts new file mode 100644 index 0000000..3910bf2 --- /dev/null +++ b/src/factory/admin/management/user.ts @@ -0,0 +1,39 @@ +import { user } from "../../../entity/management/user"; +import PermissionHelper from "../../../helpers/permissionHelper"; +import { UserViewModel } from "../../../viewmodel/admin/management/user.models"; +import RoleFactory from "./role"; + +export default abstract class UserFactory { + /** + * @description map record to user + * @param {user} record + * @returns {UserViewModel} + */ + public static mapToSingle(record: user): UserViewModel { + let userPermissionStrings = record.permissions.map((e) => e.permission); + let rolePermissions = record.roles.map((e) => e.permissions).flat(); + let rolePermissionStrings = rolePermissions.map((p) => p.permission); + let totalPermissions = PermissionHelper.convertToObject([...userPermissionStrings, ...rolePermissionStrings]); + + return { + id: record.id, + username: record.username, + firstname: record.firstname, + lastname: record.lastname, + mail: record.mail, + isOwner: record.isOwner, + permissions: PermissionHelper.convertToObject(userPermissionStrings), + roles: RoleFactory.mapToBase(record.roles), + permissions_total: totalPermissions, + }; + } + + /** + * @description map records to user + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/helpers/backupHelper.ts b/src/helpers/backupHelper.ts new file mode 100644 index 0000000..f50e307 --- /dev/null +++ b/src/helpers/backupHelper.ts @@ -0,0 +1,246 @@ +import { dataSource } from "../data-source"; +import { FileSystemHelper } from "./fileSystemHelper"; +import { EntityManager } from "typeorm"; +import uniqBy from "lodash.uniqby"; +import InternalException from "../exceptions/internalException"; +import UserService from "../service/management/userService"; +import { BACKUP_COPIES, BACKUP_INTERVAL } from "../env.defaults"; +import DatabaseActionException from "../exceptions/databaseActionException"; + +export type BackupSection = "member" | "user"; + +export type BackupSectionRefered = { + [key in BackupSection]?: Array; +}; + +export type BackupFileContent = { [key in BackupSection]?: BackupFileContentSection } & { + backup_file_details: { collectIds: boolean; createdAt: Date; version: 1 }; +}; +export type BackupFileContentSection = Array | { [key: string]: Array }; + +export default abstract class BackupHelper { + // ! Order matters because of foreign keys + private static readonly backupSection: Array<{ type: BackupSection; orderOnInsert: number; orderOnClear: number }> = [ + { type: "member", orderOnInsert: 2, orderOnClear: 2 }, + { type: "user", orderOnInsert: 1, orderOnClear: 1 }, + ]; + + private static readonly backupSectionRefered: BackupSectionRefered = { + member: ["member"], + user: ["user", "user_permission", "role", "role_permission", "invite"], + }; + + private static transactionManager: EntityManager; + + static async createBackup({ + filename, + path = "/backup", + collectIds = true, + }: { + filename?: string; + path?: string; + collectIds?: boolean; + }): Promise { + if (!filename) { + filename = new Date().toISOString().split("T")[0]; + } + + let json: BackupFileContent = { backup_file_details: { collectIds, createdAt: new Date(), version: 1 } }; + for (const section of this.backupSection) { + json[section.type] = await this.getSectionData(section.type, collectIds); + } + + FileSystemHelper.writeFile(path, filename + ".json", JSON.stringify(json, null, 2)); + + let files = FileSystemHelper.getFilesInDirectory("backup", ".json"); + let sorted = files.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime()); + + const filesToDelete = sorted.slice(BACKUP_COPIES); + for (const file of filesToDelete) { + FileSystemHelper.deleteFile("backup", file); + } + } + + static async createBackupOnInterval() { + let files = FileSystemHelper.getFilesInDirectory("backup", ".json"); + let newestFile = files.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime())[0]; + + let lastBackup = new Date(newestFile.split(".")[0]); + let diffInMs = new Date().getTime() - lastBackup.getTime(); + let diffInDays = diffInMs / (1000 * 60 * 60 * 24); + + if (diffInDays >= BACKUP_INTERVAL) { + await this.createBackup({}); + } + } + + static async loadBackup({ + filename, + path = "/backup", + include = [], + partial = false, + overwrite = false, + }: { + filename: string; + path?: string; + partial?: boolean; + include?: Array; + overwrite?: boolean; + }): Promise { + this.transactionManager = undefined; + + let file = FileSystemHelper.readFile(`${path}/${filename}`); + let backup: BackupFileContent = JSON.parse(file); + + if ((partial && include.length == 0) || (!partial && include.length != 0)) { + throw new InternalException("partial and include have to be set correctly for restoring backup."); + } + + await dataSource.manager + .transaction(async (transaction) => { + this.transactionManager = transaction; + + const sections = this.backupSection + .filter((bs) => (partial ? include.includes(bs.type) : true)) + .sort((a, b) => a.orderOnClear - b.orderOnClear); + if (!overwrite) { + for (const section of sections.filter((s) => Object.keys(backup).includes(s.type))) { + let refered = this.backupSectionRefered[section.type]; + for (const ref of refered) { + await this.transactionManager.getRepository(ref).delete({}); + } + } + } + + for (const section of sections + .filter((s) => Object.keys(backup).includes(s.type)) + .sort((a, b) => a.orderOnInsert - b.orderOnInsert)) { + await this.setSectionData(section.type, backup[section.type], backup.backup_file_details.collectIds ?? false); + } + + this.transactionManager = undefined; + }) + .catch((err) => { + console.log(err); + this.transactionManager = undefined; + throw new DatabaseActionException("BACKUP RESTORE", include.join(", ") || "FULL", err); + }); + } + + public static async autoRestoreBackup() { + let count = await UserService.count(); + if (count == 0) { + let files = FileSystemHelper.getFilesInDirectory("/backup", ".json"); + let newestFile = files.sort( + (a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime() + )[0]; + + if (newestFile) { + console.log(`${new Date().toISOString()}: auto-restoring ${newestFile}`); + await this.loadBackup({ filename: newestFile }); + console.log(`${new Date().toISOString()}: finished auto-restore`); + } else { + console.log(`${new Date().toISOString()}: skip auto-restore as no backup was found`); + } + } else { + console.log(`${new Date().toISOString()}: skip auto-restore as users exist`); + } + } + + private static async getSectionData( + section: BackupSection, + collectIds: boolean + ): Promise | { [key: string]: any }> { + switch (section) { + case "member": + return await this.getMemberData(collectIds); + case "user": + return await this.getUser(collectIds); + default: + return []; + } + } + + private static async getMemberData(collectIds: boolean): Promise> { + return await dataSource + .getRepository("member") + .createQueryBuilder("member") + .select([...(collectIds ? ["member.id"] : []), "member.firstname", "member.lastname", "member.nameaffix"]) + .getMany(); + } + private static async getUser(collectIds: boolean): Promise<{ [key: string]: Array }> { + return { + user: await dataSource + .getRepository("user") + .createQueryBuilder("user") + .leftJoin("user.roles", "roles") + .leftJoin("roles.permissions", "role_permissions") + .leftJoin("user.permissions", "permissions") + .select([ + ...(collectIds ? ["user.id"] : []), + "user.mail", + "user.username", + "user.firstname", + "user.lastname", + "user.secret", + "user.isOwner", + ]) + .addSelect(["permissions.permission"]) + .addSelect(["roles.role"]) + .addSelect(["role_permissions.permission"]) + .getMany(), + role: await dataSource + .getRepository("role") + .createQueryBuilder("role") + .leftJoin("role.permissions", "permissions") + .addSelect(["role.role"]) + .addSelect(["permissions.permission"]) + .getMany(), + invite: await dataSource.getRepository("invite").find(), + }; + } + + private static async setSectionData( + section: BackupSection, + data: BackupFileContentSection, + collectedIds: boolean + ): Promise { + if (section == "member" && Array.isArray(data)) await this.setMemberData(data); + if (section == "user" && !Array.isArray(data)) await this.setUser(data); + } + + private static async setMemberData(data: Array): Promise { + await this.transactionManager.getRepository("member").save(data); + } + private static async setUser(data: { [key: string]: Array }): Promise { + let usedRoles = (data?.["user"] ?? []) + .map((d) => d.roles) + .flat() + .map((d) => ({ ...d, id: undefined })); + + await this.transactionManager + .createQueryBuilder() + .insert() + .into("role") + .values(uniqBy([...(data?.["role"] ?? []), ...usedRoles], "role")) + .orIgnore() + .execute(); + + let roles = await this.transactionManager.getRepository("role").find(); + let dataWithMappedIds = (data?.["user"] ?? []).map((u) => ({ + ...u, + roles: u.roles.map((r: any) => ({ + ...r, + id: roles.find((role) => role.role == r.role)?.id ?? undefined, + })), + })); + await this.transactionManager.getRepository("user").save(dataWithMappedIds); + await this.transactionManager + .createQueryBuilder() + .insert() + .into("invite") + .values(data["invite"]) + .orIgnore() + .execute(); + } +} diff --git a/src/helpers/fileSystemHelper.ts b/src/helpers/fileSystemHelper.ts new file mode 100644 index 0000000..f77bef2 --- /dev/null +++ b/src/helpers/fileSystemHelper.ts @@ -0,0 +1,71 @@ +import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs"; +import { join } from "path"; +import { readdirSync } from "fs"; + +export abstract class FileSystemHelper { + static createFolder(...args: string[]) { + const exportPath = this.formatPath(...args); + if (!existsSync(exportPath)) { + mkdirSync(exportPath, { recursive: true }); + } + } + + static readFile(...filePath: string[]) { + this.createFolder(...filePath); + return readFileSync(this.formatPath(...filePath), "utf8"); + } + + static readFileasBase64(...filePath: string[]) { + this.createFolder(...filePath); + return readFileSync(this.formatPath(...filePath), "base64"); + } + + static readTemplateFile(filePath: string) { + this.createFolder(filePath); + return readFileSync(process.cwd() + filePath, "utf8"); + } + + static writeFile(filePath: string, filename: string, file: any) { + this.createFolder(filePath); + let path = this.formatPath(filePath, filename); + writeFileSync(path, file); + } + + static deleteFile(...filePath: string[]) { + const path = this.formatPath(...filePath); + if (existsSync(path)) { + unlinkSync(path); + } + } + + static formatPath(...args: string[]) { + return join(process.cwd(), "files", ...args); + } + + static normalizePath(...args: string[]) { + return join(...args); + } + + static getFilesInDirectory(directoryPath: string, filetype?: string): string[] { + const fullPath = this.formatPath(directoryPath); + if (!existsSync(fullPath)) { + return []; + } + return readdirSync(fullPath, { withFileTypes: true }) + .filter((dirent) => !dirent.isDirectory() && (!filetype || dirent.name.endsWith(filetype))) + .map((dirent) => dirent.name); + } + + static clearDirectoryByFiletype(directoryPath: string, filetype: string) { + const fullPath = this.formatPath(directoryPath); + if (!existsSync(fullPath)) { + return; + } + readdirSync(fullPath, { withFileTypes: true }) + .filter((dirent) => !dirent.isDirectory() && dirent.name.endsWith(filetype)) + .forEach((dirent) => { + const filePath = join(fullPath, dirent.name); + unlinkSync(filePath); + }); + } +} diff --git a/src/helpers/jwtHelper.ts b/src/helpers/jwtHelper.ts new file mode 100644 index 0000000..230ead0 --- /dev/null +++ b/src/helpers/jwtHelper.ts @@ -0,0 +1,78 @@ +import jwt from "jsonwebtoken"; +import { JWTData, JWTToken } from "../type/jwtTypes"; +import { JWT_SECRET, JWT_EXPIRATION } from "../env.defaults"; +import InternalException from "../exceptions/internalException"; +import RolePermissionService from "../service/management/rolePermissionService"; +import UserPermissionService from "../service/management/userPermissionService"; +import UserService from "../service/management/userService"; +import PermissionHelper from "./permissionHelper"; + +export abstract class JWTHelper { + static validate(token: string): Promise { + return new Promise((resolve, reject) => { + jwt.verify(token, JWT_SECRET, (err, decoded) => { + if (err) reject(err.message); + else resolve(decoded); + }); + }); + } + + static create( + data: JWTData, + { expOverwrite, useExpiration }: { expOverwrite?: number; useExpiration?: boolean } = { useExpiration: true } + ): Promise { + return new Promise((resolve, reject) => { + jwt.sign( + data, + JWT_SECRET, + { + ...(useExpiration ?? true ? { expiresIn: expOverwrite ?? JWT_EXPIRATION } : {}), + }, + (err, token) => { + if (err) reject(err.message); + else resolve(token); + } + ); + }); + } + + static decode(token: string): Promise { + return new Promise((resolve, reject) => { + try { + let decoded = jwt.decode(token); + resolve(decoded); + } catch (err) { + reject(err.message); + } + }); + } + + static async buildToken(id: string): Promise { + let { firstname, lastname, mail, username, isOwner } = await UserService.getById(id); + let userPermissions = await UserPermissionService.getByUser(id); + let userPermissionStrings = userPermissions.map((e) => e.permission); + let userRoles = await UserService.getAssignedRolesByUserId(id); + let rolePermissions = + userRoles.length != 0 ? await RolePermissionService.getByRoles(userRoles.map((e) => e.id)) : []; + let rolePermissionStrings = rolePermissions.map((e) => e.permission); + let permissionObject = PermissionHelper.convertToObject([...userPermissionStrings, ...rolePermissionStrings]); + + let jwtData: JWTToken = { + userId: id, + mail: mail, + username: username, + firstname: firstname, + lastname: lastname, + isOwner: isOwner, + permissions: permissionObject, + }; + + return await JWTHelper.create(jwtData) + .then((result) => { + return result; + }) + .catch((err) => { + throw new InternalException("Failed accessToken creation", err); + }); + } +} diff --git a/src/helpers/mailHelper.ts b/src/helpers/mailHelper.ts new file mode 100644 index 0000000..ab44a26 --- /dev/null +++ b/src/helpers/mailHelper.ts @@ -0,0 +1,43 @@ +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"; + +export default abstract class MailHelper { + private static readonly transporter: Transporter = createTransport({ + host: MAIL_HOST, + port: MAIL_PORT, + secure: (MAIL_SECURE as "true" | "false") == "true", + auth: { + user: MAIL_USERNAME, + pass: MAIL_PASSWORD, + }, + } as TransportOptions); + + /** + * @description send mail + * @param {string} target + * @param {string} subject + * @param {string} content + * @returns {Prmose<*>} + */ + static async sendMail( + target: string, + subject: string, + content: string, + attach: Array = [] + ): Promise { + return new Promise((resolve, reject) => { + this.transporter + .sendMail({ + from: `"${CLUB_NAME}" <${MAIL_USERNAME}>`, + to: target, + subject, + text: content, + html: content, + attachments: attach, + }) + .then((info) => resolve(info.messageId)) + .catch((e) => reject(e)); + }); + } +} diff --git a/src/helpers/parameterPassCheckHelper.ts b/src/helpers/parameterPassCheckHelper.ts new file mode 100644 index 0000000..16ba5f0 --- /dev/null +++ b/src/helpers/parameterPassCheckHelper.ts @@ -0,0 +1,28 @@ +import { Request, Response } from "express"; +import BadRequestException from "../exceptions/badRequestException"; + +export default class ParamaterPassCheckHelper { + static requiredIncluded(testfor: Array, obj: object) { + let result = testfor.every((key) => Object.keys(obj).includes(key)); + if (!result) throw new BadRequestException(`not all required parameters included: ${testfor.join(",")}`); + } + + static forbiddenIncluded(testfor: Array, obj: object) { + let result = testfor.some((key) => Object.keys(obj).includes(key)); + if (!result) throw new BadRequestException(`PPC: forbidden parameters included: ${testfor.join(",")}`); + } + + static requiredIncludedMiddleware(testfor: Array): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + this.requiredIncluded(testfor, req.body); + next(); + }; + } + + static forbiddenIncludedMiddleware(testfor: Array): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + this.requiredIncluded(testfor, req.body); + next(); + }; + } +} diff --git a/src/helpers/permissionHelper.ts b/src/helpers/permissionHelper.ts new file mode 100644 index 0000000..86b27e2 --- /dev/null +++ b/src/helpers/permissionHelper.ts @@ -0,0 +1,255 @@ +import { Request, Response } from "express"; +import { + PermissionModule, + permissionModules, + PermissionObject, + PermissionSection, + PermissionString, + PermissionType, + permissionTypes, +} from "../type/permissionTypes"; +import ForbiddenRequestException from "../exceptions/forbiddenRequestException"; + +export default class PermissionHelper { + static can( + permissions: PermissionObject, + type: PermissionType | "admin", + section: PermissionSection, + module?: PermissionModule + ) { + if (type == "admin") return permissions?.admin ?? false; + if (permissions?.admin) return true; + if ( + (!module && + permissions[section] != undefined && + (permissions[section]?.all == "*" || permissions[section]?.all?.includes(type))) || + permissions[section]?.all == "*" || + permissions[section]?.all?.includes(type) + ) + return true; + if (module && (permissions[section]?.[module] == "*" || permissions[section]?.[module]?.includes(type))) + return true; + return false; + } + + static canSome( + permissions: PermissionObject, + checks: Array<{ + requiredPermissions: PermissionType | "admin"; + section: PermissionSection; + module?: PermissionModule; + }> + ) { + checks.reduce((prev, curr) => { + return prev || this.can(permissions, curr.requiredPermissions, curr.section, curr.module); + }, false); + } + + static canSection( + permissions: PermissionObject, + type: PermissionType | "admin", + section: PermissionSection + ): boolean { + if (type == "admin") return permissions?.admin ?? false; + if (permissions?.admin) return true; + if ( + permissions[section]?.all == "*" || + permissions[section]?.all?.includes(type) || + permissions[section] != undefined + ) + return true; + return false; + } + + static canSomeSection( + permissions: PermissionObject, + checks: Array<{ + requiredPermissions: PermissionType | "admin"; + section: PermissionSection; + }> + ): boolean { + return checks.reduce((prev, curr) => { + return prev || this.can(permissions, curr.requiredPermissions, curr.section); + }, false); + } + + static passCheckMiddleware( + requiredPermissions: PermissionType | "admin", + section: PermissionSection, + module?: PermissionModule + ): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + const permissions = req.permissions; + const isOwner = req.isOwner; + + if (isOwner || this.can(permissions, requiredPermissions, section, module)) { + next(); + } else { + throw new ForbiddenRequestException(`missing permission for ${section}.${module}.${requiredPermissions}`); + } + }; + } + + static passCheckSomeMiddleware( + checks: Array<{ + requiredPermissions: PermissionType | "admin"; + section: PermissionSection; + module?: PermissionModule; + }> + ): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + const permissions = req.permissions; + const isOwner = req.isOwner; + + if (isOwner || this.canSome(permissions, checks)) { + next(); + } else { + let permissionsToPass = checks.reduce((prev, curr) => { + return prev + (prev != " or " ? "" : "") + `${curr.section}.${curr.module}.${curr.requiredPermissions}`; + }, ""); + throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`); + } + }; + } + + static sectionPassCheckMiddleware( + requiredPermissions: PermissionType | "admin", + section: PermissionSection + ): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + const permissions = req.permissions; + const isOwner = req.isOwner; + + if (isOwner || this.canSection(permissions, requiredPermissions, section)) { + next(); + } else { + throw new ForbiddenRequestException(`missing permission for ${section}.${module}.${requiredPermissions}`); + } + }; + } + + static sectionPassCheckSomeMiddleware( + checks: Array<{ requiredPermissions: PermissionType | "admin"; section: PermissionSection }> + ): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + const permissions = req.permissions; + const isOwner = req.isOwner; + + if (isOwner || this.canSomeSection(permissions, checks)) { + next(); + } else { + let permissionsToPass = checks.reduce((prev, curr) => { + return prev + (prev != " or " ? "" : "") + `${curr.section}.${curr.requiredPermissions}`; + }, ""); + throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`); + } + }; + } + + static isAdminMiddleware(): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + const permissions = req.permissions; + const isOwner = req.isOwner; + + if (isOwner || permissions.admin) { + next(); + } else { + throw new ForbiddenRequestException(`missing admin permission`); + } + }; + } + + static convertToObject(permissions: Array): PermissionObject { + if (permissions.includes("*")) { + return { + admin: true, + }; + } + let output: PermissionObject = {}; + let splitPermissions = permissions.map((e) => e.split(".")) as Array< + [PermissionSection, PermissionModule | PermissionType | "*", PermissionType | "*"] + >; + for (let split of splitPermissions) { + if (!output[split[0]]) { + output[split[0]] = {}; + } + if (split[1] == "*" || output[split[0]].all == "*") { + output[split[0]] = { all: "*" }; + } else if (permissionTypes.includes(split[1] as PermissionType)) { + if (!output[split[0]].all || !Array.isArray(output[split[0]].all)) { + output[split[0]].all = []; + } + const permissionIndex = permissionTypes.indexOf(split[1] as PermissionType); + const appliedPermissions = permissionTypes.slice(0, permissionIndex + 1); + + if (output[split[0]].all != "*") { + output[split[0]].all = [ + ...new Set([...output[split[0]].all, ...appliedPermissions]), + ] as Array; + } + } else { + if (split[2] == "*" || output[split[0]][split[1] as PermissionModule] == "*") { + output[split[0]][split[1] as PermissionModule] = "*"; + } else { + if ( + !output[split[0]][split[1] as PermissionModule] || + !Array.isArray(output[split[0]][split[1] as PermissionModule]) + ) { + output[split[0]][split[1] as PermissionModule] = []; + } + const permissionIndex = permissionTypes.indexOf(split[2] as PermissionType); + const appliedPermissions = permissionTypes.slice(0, permissionIndex + 1); + output[split[0]][split[1] as PermissionModule] = appliedPermissions; + if (output[split[0]][split[1] as PermissionModule] != "*") { + output[split[0]][split[1] as PermissionModule] = [ + ...new Set([...output[split[0]][split[1] as PermissionModule], ...appliedPermissions]), + ] as Array; + } + } + } + } + return output; + } + + static convertToStringArray(permissions: PermissionObject): Array { + if (permissions?.admin) { + return ["*"]; + } + let output: Array = []; + let sections = Object.keys(permissions) as Array; + for (let section of sections) { + if (permissions[section].all) { + let types = permissions[section].all; + if (types == "*" || types.length == permissionTypes.length) { + output.push(`${section}.*`); + } else { + for (let type of types) { + output.push(`${section}.${type}`); + } + } + } + let modules = Object.keys(permissions[section]).filter((m: PermissionModule) => + permissionModules.includes(m) + ) as Array; + for (let module of modules) { + let types = permissions[section][module]; + if (types == "*" || types.length == permissionTypes.length) { + output.push(`${section}.${module}.*`); + } else { + for (let type of types) { + output.push(`${section}.${module}.${type}`); + } + } + } + } + return output; + } + + static getWhatToAdd(before: Array, after: Array): Array { + return after.filter((permission) => !before.includes(permission)); + } + + static getWhatToRemove(before: Array, after: Array): Array { + return before.filter((permission) => !after.includes(permission)); + } +} diff --git a/src/helpers/stringHelper.ts b/src/helpers/stringHelper.ts new file mode 100644 index 0000000..f41c660 --- /dev/null +++ b/src/helpers/stringHelper.ts @@ -0,0 +1,17 @@ +import crypto from "crypto"; + +export abstract class StringHelper { + static random(len: number, charSet?: string): string { + // charSet = charSet || "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + // var randomString = ""; + // for (var i = 0; i < len; i++) { + // var randomPoz = Math.floor(Math.random() * charSet.length); + // randomString += charSet.substring(randomPoz, randomPoz + 1); + // } + // return randomString; + return crypto + .randomBytes(len) + .toString("base64") + .replace(/[^a-zA-Z0-9]/g, ""); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..3517a6b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,44 @@ +import "dotenv/config"; +import express from "express"; + +import { BACKUP_AUTO_RESTORE, configCheck, SERVER_PORT } from "./env.defaults"; +configCheck(); + +import { PermissionObject } from "./type/permissionTypes"; +declare global { + namespace Express { + export interface Request { + userId: string; + username: string; + isOwner: boolean; + permissions: PermissionObject; + isPWA: boolean; + isWebApiRequest: boolean; + } + } +} + +import { dataSource } from "./data-source"; +import BackupHelper from "./helpers/backupHelper"; +dataSource.initialize().then(async () => { + if ((BACKUP_AUTO_RESTORE as "true" | "false") == "true" && (await dataSource.createQueryRunner().hasTable("user"))) { + await BackupHelper.autoRestoreBackup().catch((err) => { + console.log(`${new Date().toISOString()}: failed auto-restoring database`, err); + }); + } +}); + +const app = express(); +import router from "./routes/index"; +router(app); +app.listen(process.env.NODE_ENV ? SERVER_PORT : 5000, () => { + console.log(`${new Date().toISOString()}: listening on port ${process.env.NODE_ENV ? SERVER_PORT : 5000}`); +}); + +import schedule from "node-schedule"; +import RefreshCommandHandler from "./command/refreshCommandHandler"; +const job = schedule.scheduleJob("0 0 * * *", async () => { + console.log(`${new Date().toISOString()}: running Cron`); + await RefreshCommandHandler.deleteExpired(); + await BackupHelper.createBackupOnInterval(); +}); diff --git a/src/middleware/allowSetup.ts b/src/middleware/allowSetup.ts new file mode 100644 index 0000000..10d53fa --- /dev/null +++ b/src/middleware/allowSetup.ts @@ -0,0 +1,12 @@ +import { NextFunction, Request, Response } from "express"; +import UserService from "../service/management/userService"; +import CustomRequestException from "../exceptions/customRequestException"; + +export default async function allowSetup(req: Request, res: Response, next: NextFunction) { + let count = await UserService.count(); + if (count != 0) { + throw new CustomRequestException(405, "service is already set up"); + } + + next(); +} diff --git a/src/middleware/authenticate.ts b/src/middleware/authenticate.ts new file mode 100644 index 0000000..abeb832 --- /dev/null +++ b/src/middleware/authenticate.ts @@ -0,0 +1,43 @@ +import { NextFunction, Request, Response } from "express"; +import jwt from "jsonwebtoken"; +import BadRequestException from "../exceptions/badRequestException"; +import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; +import InternalException from "../exceptions/internalException"; +import { JWTHelper } from "../helpers/jwtHelper"; + +export default async function authenticate(req: Request, res: Response, next: NextFunction) { + const bearer = req.headers.authorization?.split(" ")?.[1] ?? undefined; + + if (!bearer) { + throw new BadRequestException("Provide valid Authorization Header"); + } + + let decoded: string | jwt.JwtPayload; + await JWTHelper.validate(bearer) + .then((result) => { + decoded = result; + }) + .catch((err) => { + if (err == "jwt expired") { + throw new UnauthorizedRequestException("Token expired", err); + } else { + throw new BadRequestException("Failed Authorization Header decoding", err); + } + }); + + if (typeof decoded == "string" || !decoded) { + throw new InternalException("process failed"); + } + + if (decoded?.sub == "api_token_retrieve") { + throw new BadRequestException("This token is only authorized to get temporary access tokens via GET /api/webapi"); + } + + req.userId = decoded.userId; + req.username = decoded.username; + req.isOwner = decoded.isOwner; + req.permissions = decoded.permissions; + req.isWebApiRequest = decoded?.sub == "webapi_access_token"; + + next(); +} diff --git a/src/middleware/detectPWA.ts b/src/middleware/detectPWA.ts new file mode 100644 index 0000000..0aa3b0f --- /dev/null +++ b/src/middleware/detectPWA.ts @@ -0,0 +1,11 @@ +import { NextFunction, Request, Response } from "express"; + +export default async function detectPWA(req: Request, res: Response, next: NextFunction) { + const userAgent = req.headers["user-agent"] || ""; + if ((userAgent.includes("Mobile") && userAgent.includes("Standalone")) || req.headers["x-pwa-client"] === "true") { + req.isPWA = true; + } else { + req.isPWA = false; + } + next(); +} diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts new file mode 100644 index 0000000..0ffe741 --- /dev/null +++ b/src/middleware/errorHandler.ts @@ -0,0 +1,22 @@ +import { NextFunction, Request, Response } from "express"; +import { ExceptionBase } from "../exceptions/exceptionsBaseType"; +import CustomRequestException from "../exceptions/customRequestException"; +import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; + +export default function errorHandler(err: ExceptionBase | Error, req: Request, res: Response, next: NextFunction) { + let status = 500; + let msg = "Internal Server Error"; + + if (err instanceof CustomRequestException) { + status = err.statusCode; + msg = err.message; + } + + if (err instanceof CustomRequestException) { + console.log("Custom Handler", status, msg); + } else { + console.log("Error Handler", err); + } + + res.status(status).send(msg); +} diff --git a/src/migrations/1739697068682-CreateSchema.ts b/src/migrations/1739697068682-CreateSchema.ts new file mode 100644 index 0000000..821b9e0 --- /dev/null +++ b/src/migrations/1739697068682-CreateSchema.ts @@ -0,0 +1,42 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { + invite_table, + refresh_table, + reset_table, + role_permission_table, + role_table, + user_permission_table, + user_roles_table, + user_table, +} from "./baseSchemaTables/admin"; +import { member_table } from "./baseSchemaTables/member"; + +export class CreateSchema1739697068682 implements MigrationInterface { + name = "CreateSchema1739697068682"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable(reset_table, true, true, true); + await queryRunner.createTable(invite_table, true, true, true); + await queryRunner.createTable(role_table, true, true, true); + await queryRunner.createTable(role_permission_table, true, true, true); + await queryRunner.createTable(user_table, true, true, true); + await queryRunner.createTable(user_roles_table, true, true, true); + await queryRunner.createTable(user_permission_table, true, true, true); + await queryRunner.createTable(refresh_table, true, true, true); + + await queryRunner.createTable(member_table, true, true, true); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("member", true, true, true); + + await queryRunner.dropTable("refresh", true, true, true); + await queryRunner.dropTable("user_permission", true, true, true); + await queryRunner.dropTable("user_roles", true, true, true); + await queryRunner.dropTable("user", true, true, true); + await queryRunner.dropTable("role_permission", true, true, true); + await queryRunner.dropTable("role", true, true, true); + await queryRunner.dropTable("invite", true, true, true); + await queryRunner.dropTable("reset", true, true, true); + } +} diff --git a/src/migrations/baseSchemaTables/admin.ts b/src/migrations/baseSchemaTables/admin.ts new file mode 100644 index 0000000..cb126bd --- /dev/null +++ b/src/migrations/baseSchemaTables/admin.ts @@ -0,0 +1,121 @@ +import { Table, TableForeignKey } from "typeorm"; +import { getDefaultByORM, getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "../ormHelper"; + +export const invite_table = new Table({ + name: "invite", + columns: [ + { name: "mail", ...getTypeByORM("varchar"), isPrimary: true }, + { name: "token", ...getTypeByORM("varchar") }, + { name: "username", ...getTypeByORM("varchar") }, + { name: "firstname", ...getTypeByORM("varchar") }, + { name: "lastname", ...getTypeByORM("varchar") }, + { name: "secret", ...getTypeByORM("varchar") }, + ], +}); +export const role_table = new Table({ + name: "role", + columns: [ + { name: "id", ...getTypeByORM("int"), ...isIncrementPrimary }, + { name: "role", ...getTypeByORM("varchar"), isUnique: true }, + ], +}); + +export const role_permission_table = new Table({ + name: "role_permission", + columns: [ + { name: "roleId", ...getTypeByORM("int"), isPrimary: true }, + { name: "permission", ...getTypeByORM("varchar"), isPrimary: true }, + ], + foreignKeys: [ + new TableForeignKey({ + columnNames: ["roleId"], + referencedColumnNames: ["id"], + referencedTableName: "role", + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }), + ], +}); + +export const user_table = new Table({ + name: "user", + columns: [ + { name: "id", ...getTypeByORM("uuid"), ...isUUIDPrimary }, + { name: "mail", ...getTypeByORM("varchar"), isUnique: true }, + { name: "username", ...getTypeByORM("varchar"), isUnique: true }, + { name: "firstname", ...getTypeByORM("varchar") }, + { name: "lastname", ...getTypeByORM("varchar") }, + { name: "secret", ...getTypeByORM("varchar") }, + { name: "static", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) }, + { name: "isOwner", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) }, + ], +}); + +export const user_roles_table = new Table({ + name: "user_roles", + columns: [ + { name: "userId", ...getTypeByORM("uuid"), isPrimary: true }, + { name: "roleId", ...getTypeByORM("int"), isPrimary: true }, + ], + foreignKeys: [ + new TableForeignKey({ + columnNames: ["userId"], + referencedColumnNames: ["id"], + referencedTableName: "user", + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }), + new TableForeignKey({ + columnNames: ["roleId"], + referencedColumnNames: ["id"], + referencedTableName: "role", + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }), + ], +}); + +export const user_permission_table = new Table({ + name: "user_permission", + columns: [ + { name: "userId", ...getTypeByORM("uuid"), isPrimary: true }, + { name: "permission", ...getTypeByORM("varchar"), isPrimary: true }, + ], + foreignKeys: [ + new TableForeignKey({ + columnNames: ["userId"], + referencedColumnNames: ["id"], + referencedTableName: "user", + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }), + ], +}); + +export const refresh_table = new Table({ + name: "refresh", + columns: [ + { name: "token", ...getTypeByORM("varchar"), isPrimary: true }, + { name: "expiry", ...getTypeByORM("datetime", false, 6) }, + { name: "userId", ...getTypeByORM("uuid"), isPrimary: true }, + ], + foreignKeys: [ + new TableForeignKey({ + columnNames: ["userId"], + referencedColumnNames: ["id"], + referencedTableName: "user", + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }), + ], +}); + +export const reset_table = new Table({ + name: "reset", + columns: [ + { name: "mail", ...getTypeByORM("varchar"), isPrimary: true }, + { name: "token", ...getTypeByORM("varchar") }, + { name: "username", ...getTypeByORM("varchar") }, + { name: "secret", ...getTypeByORM("varchar") }, + ], +}); diff --git a/src/migrations/baseSchemaTables/member.ts b/src/migrations/baseSchemaTables/member.ts new file mode 100644 index 0000000..7805277 --- /dev/null +++ b/src/migrations/baseSchemaTables/member.ts @@ -0,0 +1,12 @@ +import { Table } from "typeorm"; +import { getTypeByORM, isUUIDPrimary } from "../ormHelper"; + +export const member_table = new Table({ + name: "member", + columns: [ + { name: "id", ...getTypeByORM("uuid"), ...isUUIDPrimary }, + { name: "firstname", ...getTypeByORM("varchar") }, + { name: "lastname", ...getTypeByORM("varchar") }, + { name: "nameaffix", ...getTypeByORM("varchar") }, + ], +}); diff --git a/src/migrations/ormHelper.ts b/src/migrations/ormHelper.ts new file mode 100644 index 0000000..20a57ad --- /dev/null +++ b/src/migrations/ormHelper.ts @@ -0,0 +1,105 @@ +export type ORMType = "int" | "bigint" | "boolean" | "date" | "datetime" | "time" | "text" | "varchar" | "uuid"; +export type ORMDefault = "currentTimestamp" | "string" | "boolean" | "number" | "null"; +export type ColumnConfig = { + type: string; + length?: string; + precision?: number; + isNullable: boolean; +}; +export type Primary = { + isPrimary: boolean; + isGenerated: boolean; + generationStrategy: "increment" | "uuid" | "rowid" | "identity"; +}; + +export function getTypeByORM(type: ORMType, nullable: boolean = false, length: number = 255): ColumnConfig { + const dbType = process.env.DB_TYPE; + + const typeMap: Record> = { + mysql: { + int: "int", + bigint: "bigint", + boolean: "tinyint", + date: "date", + datetime: "datetime", + time: "time", + text: "text", + varchar: "varchar", + uuid: "varchar", + }, + postgres: { + int: "integer", + bigint: "bigint", + boolean: "boolean", + date: "date", + datetime: "timestamp", + time: "time", + text: "text", + varchar: "character varying", + uuid: "uuid", + }, + sqlite: { + int: "integer", + bigint: "integer", + boolean: "integer", + date: "date", + datetime: "datetime", + time: "text", + text: "text", + varchar: "varchar", + uuid: "varchar", + }, + }; + + let obj: ColumnConfig = { + type: typeMap[dbType]?.[type] || type, + isNullable: nullable, + }; + if (type == "datetime") obj.precision = length; + else if (dbType != "sqlite" && (obj.type == "varchar" || type == "varchar")) obj.length = `${length}`; + else if (dbType != "postgres" && type == "uuid") obj.length = "36"; + + return obj; +} + +export function getDefaultByORM(type: ORMDefault, data?: string | number | boolean): T { + const dbType = process.env.DB_TYPE; + + const typeMap: Record> = { + mysql: { + currentTimestamp: `CURRENT_TIMESTAMP(${data ?? 6})`, + string: `'${data ?? ""}'`, + boolean: Boolean(data).toString(), + number: Number(data).toString(), + null: null, + }, + postgres: { + currentTimestamp: `now()`, + string: `'${data ?? ""}'`, + boolean: Boolean(data) == true ? "true" : "false", + number: Number(data).toString(), + null: null, + }, + sqlite: { + currentTimestamp: `datetime('now')`, + string: `'${data ?? ""}'`, + boolean: Boolean(data) == true ? "1" : "0", + number: Number(data).toString(), + null: null, + }, + }; + + return (typeMap[dbType]?.[type] || type) as T; +} + +export const isIncrementPrimary: Primary = { + isPrimary: true, + isGenerated: true, + generationStrategy: "increment", +}; + +export const isUUIDPrimary: Primary = { + isPrimary: true, + isGenerated: true, + generationStrategy: "uuid", +}; diff --git a/src/routes/admin/configuration/member.ts b/src/routes/admin/configuration/member.ts new file mode 100644 index 0000000..661a3b4 --- /dev/null +++ b/src/routes/admin/configuration/member.ts @@ -0,0 +1,50 @@ +import express, { Request, Response } from "express"; +import { + createMember, + deleteMemberById, + getAllMembers, + getMemberById, + getMembersByIds, + updateMemberById, +} from "../../../controller/admin/configuration/memberController"; +import PermissionHelper from "../../../helpers/permissionHelper"; + +var router = express.Router({ mergeParams: true }); + +router.get("/", async (req: Request, res: Response) => { + await getAllMembers(req, res); +}); + +router.post("/ids", async (req: Request, res: Response) => { + await getMembersByIds(req, res); +}); + +router.get("/:id", async (req: Request, res: Response) => { + await getMemberById(req, res); +}); + +router.post( + "/", + PermissionHelper.passCheckMiddleware("create", "operation", "force"), + async (req: Request, res: Response) => { + await createMember(req, res); + } +); + +router.patch( + "/:id", + PermissionHelper.passCheckMiddleware("update", "operation", "force"), + async (req: Request, res: Response) => { + await updateMemberById(req, res); + } +); + +router.delete( + "/:id", + PermissionHelper.passCheckMiddleware("delete", "operation", "force"), + async (req: Request, res: Response) => { + await deleteMemberById(req, res); + } +); + +export default router; diff --git a/src/routes/admin/index.ts b/src/routes/admin/index.ts new file mode 100644 index 0000000..68d488c --- /dev/null +++ b/src/routes/admin/index.ts @@ -0,0 +1,27 @@ +import express from "express"; +import PermissionHelper from "../../helpers/permissionHelper"; + +import member from "./configuration/member"; + +import role from "./management/role"; +import user from "./management/user"; +import invite from "./management/invite"; +import backup from "./management/backup"; + +var router = express.Router({ mergeParams: true }); + +router.use("/member", PermissionHelper.passCheckMiddleware("read", "configuration", "force"), member); + +router.use("/role", PermissionHelper.passCheckMiddleware("read", "management", "role"), role); +router.use( + "/user", + PermissionHelper.passCheckSomeMiddleware([ + { requiredPermissions: "read", section: "management", module: "user" }, + { requiredPermissions: "read", section: "management", module: "role" }, + ]), + user +); +router.use("/invite", PermissionHelper.passCheckMiddleware("read", "management", "user"), invite); +router.use("/backup", PermissionHelper.passCheckMiddleware("read", "management", "backup"), backup); + +export default router; diff --git a/src/routes/admin/management/backup.ts b/src/routes/admin/management/backup.ts new file mode 100644 index 0000000..f093554 --- /dev/null +++ b/src/routes/admin/management/backup.ts @@ -0,0 +1,88 @@ +import express, { Request, Response } from "express"; +import PermissionHelper from "../../../helpers/permissionHelper"; +import multer from "multer"; +import { + createManualBackup, + downloadBackupFile, + downloadUploadedBackupFile, + getGeneratedBackups, + getUploadedBackups, + restoreBackupByLocalFile, + restoreBackupByUploadedFile, + uploadBackupFile, +} from "../../../controller/admin/management/backupController"; +import { FileSystemHelper } from "../../../helpers/fileSystemHelper"; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + FileSystemHelper.createFolder("uploaded-backup"); + cb(null, "files/uploaded-backup/"); + }, + filename: (req, file, cb) => { + cb(null, `${new Date().toISOString().split("T")[0]}_${file.originalname}`); + }, +}); + +const upload = multer({ + storage, + fileFilter: (req: Request, file, cb) => { + if (file.mimetype === "application/json") { + cb(null, true); + } else { + cb(new Error("Only JSON files are allowed!")); + } + }, +}); + +var router = express.Router({ mergeParams: true }); + +router.get("/generated", async (req: Request, res: Response) => { + await getGeneratedBackups(req, res); +}); + +router.get("/generated/:filename", async (req: Request, res: Response) => { + await downloadBackupFile(req, res); +}); + +router.get("/uploaded", async (req: Request, res: Response) => { + await getUploadedBackups(req, res); +}); + +router.get("/uploaded/:filename", async (req: Request, res: Response) => { + await downloadUploadedBackupFile(req, res); +}); + +router.post( + "/", + PermissionHelper.passCheckMiddleware("create", "management", "backup"), + async (req: Request, res: Response) => { + await createManualBackup(req, res); + } +); + +router.post( + "/generated/restore", + PermissionHelper.passCheckMiddleware("admin", "management", "backup"), + async (req: Request, res: Response) => { + await restoreBackupByLocalFile(req, res); + } +); + +router.post( + "/uploaded/restore", + PermissionHelper.passCheckMiddleware("admin", "management", "backup"), + async (req: Request, res: Response) => { + await restoreBackupByUploadedFile(req, res); + } +); + +router.post( + "/upload", + PermissionHelper.passCheckMiddleware("create", "management", "backup"), + upload.single("file"), + async (req: Request, res: Response) => { + await uploadBackupFile(req, res); + } +); + +export default router; diff --git a/src/routes/admin/management/invite.ts b/src/routes/admin/management/invite.ts new file mode 100644 index 0000000..3f6f7d8 --- /dev/null +++ b/src/routes/admin/management/invite.ts @@ -0,0 +1,27 @@ +import express, { Request, Response } from "express"; +import PermissionHelper from "../../../helpers/permissionHelper"; +import { deleteInvite, getInvites, inviteUser } from "../../../controller/inviteController"; + +var router = express.Router({ mergeParams: true }); + +router.get("/", async (req: Request, res: Response) => { + await getInvites(req, res); +}); + +router.post( + "/", + PermissionHelper.passCheckMiddleware("create", "management", "user"), + async (req: Request, res: Response) => { + await inviteUser(req, res); + } +); + +router.delete( + "/:mail", + PermissionHelper.passCheckMiddleware("delete", "management", "user"), + async (req: Request, res: Response) => { + await deleteInvite(req, res); + } +); + +export default router; diff --git a/src/routes/admin/management/role.ts b/src/routes/admin/management/role.ts new file mode 100644 index 0000000..1087920 --- /dev/null +++ b/src/routes/admin/management/role.ts @@ -0,0 +1,59 @@ +import express, { Request, Response } from "express"; +import PermissionHelper from "../../../helpers/permissionHelper"; +import { + createRole, + deleteRole, + getAllRoles, + getRoleById, + getRolePermissions, + updateRole, + updateRolePermissions, +} from "../../../controller/admin/management/roleController"; + +var router = express.Router({ mergeParams: true }); + +router.get("/", async (req: Request, res: Response) => { + await getAllRoles(req, res); +}); + +router.get("/:id", async (req: Request, res: Response) => { + await getRoleById(req, res); +}); + +router.get("/:id/permissions", async (req: Request, res: Response) => { + await getRolePermissions(req, res); +}); + +router.post( + "/", + PermissionHelper.passCheckMiddleware("create", "management", "role"), + async (req: Request, res: Response) => { + await createRole(req, res); + } +); + +router.patch( + "/:id", + PermissionHelper.passCheckMiddleware("update", "management", "role"), + async (req: Request, res: Response) => { + await updateRole(req, res); + } +); + +router.patch( + "/:id/permissions", + PermissionHelper.passCheckMiddleware("admin", "management", "role"), + async (req: Request, res: Response) => { + await updateRolePermissions(req, res); + } +); + +router.delete( + "/:id", + PermissionHelper.passCheckMiddleware("delete", "management", "role"), + async (req: Request, res: Response) => { + await deleteRole(req, res); + } +); + +export default router; diff --git a/src/routes/admin/management/user.ts b/src/routes/admin/management/user.ts new file mode 100644 index 0000000..3a419d6 --- /dev/null +++ b/src/routes/admin/management/user.ts @@ -0,0 +1,65 @@ +import express, { Request, Response } from "express"; +import PermissionHelper from "../../../helpers/permissionHelper"; +import { + deleteUser, + getAllUsers, + getUserById, + getUserPermissions, + getUserRoles, + updateUser, + updateUserPermissions, + updateUserRoles, +} from "../../../controller/admin/management/userController"; +import { inviteUser } from "../../../controller/inviteController"; + +var router = express.Router({ mergeParams: true }); + +router.get("/", async (req: Request, res: Response) => { + await getAllUsers(req, res); +}); + +router.get("/:id", async (req: Request, res: Response) => { + await getUserById(req, res); +}); + +router.get("/:id/permissions", async (req: Request, res: Response) => { + await getUserPermissions(req, res); +}); + +router.get("/:id/roles", async (req: Request, res: Response) => { + await getUserRoles(req, res); +}); + +router.patch( + "/:id", + PermissionHelper.passCheckMiddleware("update", "management", "user"), + async (req: Request, res: Response) => { + await updateUser(req, res); + } +); + +router.patch( + "/:id/permissions", + PermissionHelper.passCheckMiddleware("admin", "management", "user"), + async (req: Request, res: Response) => { + await updateUserPermissions(req, res); + } +); + +router.patch( + "/:id/roles", + PermissionHelper.passCheckMiddleware("update", "management", "user"), + async (req: Request, res: Response) => { + await updateUserRoles(req, res); + } +); + +router.delete( + "/:id", + PermissionHelper.passCheckMiddleware("delete", "management", "user"), + async (req: Request, res: Response) => { + await deleteUser(req, res); + } +); + +export default router; diff --git a/src/routes/auth.ts b/src/routes/auth.ts new file mode 100644 index 0000000..b1200bc --- /dev/null +++ b/src/routes/auth.ts @@ -0,0 +1,18 @@ +import express from "express"; +import { login, logout, refresh } from "../controller/authController"; + +var router = express.Router({ mergeParams: true }); + +router.post("/login", async (req, res) => { + await login(req, res); +}); + +router.post("/logout", async (req, res) => { + await logout(req, res); +}); + +router.post("/refresh", async (req, res) => { + await refresh(req, res); +}); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..c3708e3 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,88 @@ +import express from "express"; +import type { Express, NextFunction, Request, RequestHandler, Response } from "express"; +import cors from "cors"; +import helmet from "helmet"; +import morgan from "morgan"; +import rateLimit from "express-rate-limit"; + +import allowSetup from "../middleware/allowSetup"; +import authenticate from "../middleware/authenticate"; +import errorHandler from "../middleware/errorHandler"; + +import setup from "./setup"; +import invite from "./invite"; +import reset from "./reset"; +import auth from "./auth"; +import admin from "./admin/index"; +import user from "./user"; +import detectPWA from "../middleware/detectPWA"; +import server from "./server"; +import PermissionHelper from "../helpers/permissionHelper"; +import ms from "ms"; +import { + SECURITY_LIMIT_REQUEST_COUNT, + SECURITY_LIMIT_WINDOW, + SECURITY_STRICT_LIMIT_REQUEST_COUNT, + SECURITY_STRICT_LIMIT_WINDOW, + TRUST_PROXY, + USE_SECURITY_LIMIT, + USE_SECURITY_STRICT_LIMIT, +} from "../env.defaults"; + +const strictLimiter = rateLimit({ + windowMs: ms(SECURITY_STRICT_LIMIT_WINDOW), + max: SECURITY_STRICT_LIMIT_REQUEST_COUNT, + message: `Zu viele Anmeldeversuche innerhalb von ${SECURITY_STRICT_LIMIT_WINDOW}. Bitte warten.`, + skipSuccessfulRequests: true, + skip: () => { + return USE_SECURITY_STRICT_LIMIT == "false"; + }, +}); + +const generalLimiter = rateLimit({ + windowMs: ms(SECURITY_LIMIT_WINDOW), + max: SECURITY_LIMIT_REQUEST_COUNT, + message: `Zu viele Anfragen innerhalb von ${SECURITY_LIMIT_WINDOW}. Bitte warten.`, + skipSuccessfulRequests: true, + skip: () => { + return USE_SECURITY_LIMIT == "false"; + }, +}); + +function excludePaths(middleware: RequestHandler, excludedPaths: Array) { + return (req: Request, res: Response, next: NextFunction) => { + if (excludedPaths.includes(req.path)) { + return next(); + } + return middleware(req, res, next); + }; +} + +export default (app: Express) => { + if (TRUST_PROXY) { + app.set("trust proxy", TRUST_PROXY); + } + app.set("query parser", "extended"); + app.use(cors()); + app.options("*", cors()); + app.use(helmet()); + app.use(morgan("short")); + app.use(express.json()); + app.use( + express.urlencoded({ + extended: true, + }) + ); + + app.use(detectPWA); + app.use("/api/setup", strictLimiter, allowSetup, setup); + app.use("/api/reset", strictLimiter, reset); + app.use("/api/invite", strictLimiter, invite); + app.use("/api/auth", strictLimiter, auth); + app.use(authenticate); + app.use(excludePaths(generalLimiter, ["/synchronize"])); + app.use("/api/admin", admin); + app.use("/api/user", user); + app.use("/api/server", PermissionHelper.isAdminMiddleware(), server); + app.use(errorHandler); +}; diff --git a/src/routes/invite.ts b/src/routes/invite.ts new file mode 100644 index 0000000..783ecef --- /dev/null +++ b/src/routes/invite.ts @@ -0,0 +1,16 @@ +import express from "express"; +import { isSetup } from "../controller/setupController"; +import { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController"; +import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper"; + +var router = express.Router({ mergeParams: true }); + +router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => { + await verifyInvite(req, res); +}); + +router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => { + await finishInvite(req, res); +}); + +export default router; diff --git a/src/routes/reset.ts b/src/routes/reset.ts new file mode 100644 index 0000000..acb1516 --- /dev/null +++ b/src/routes/reset.ts @@ -0,0 +1,19 @@ +import express from "express"; +import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper"; +import { finishReset, startReset, verifyReset } from "../controller/resetController"; + +var router = express.Router({ mergeParams: true }); + +router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => { + await verifyReset(req, res); +}); + +router.post("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["username"]), async (req, res) => { + await startReset(req, res); +}); + +router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => { + await finishReset(req, res); +}); + +export default router; diff --git a/src/routes/server.ts b/src/routes/server.ts new file mode 100644 index 0000000..0c7001a --- /dev/null +++ b/src/routes/server.ts @@ -0,0 +1,35 @@ +import express, { Request, Response } from "express"; +import { FileSystemHelper } from "../helpers/fileSystemHelper"; +import Parser from "rss-parser"; + +var router = express.Router({ mergeParams: true }); + +router.get("/version", async (req: Request, res: Response) => { + let serverPackage = FileSystemHelper.readTemplateFile("/package.json"); + let serverJson = JSON.parse(serverPackage); + res.send({ + name: serverJson.name, + description: serverJson.description, + version: serverJson.version, + author: serverJson.author, + license: serverJson.license, + }); +}); + +router.get("/settings", async (req: Request, res: Response) => { + res.json({}); +}); + +router.get("/serverrss", async (req: Request, res: Response) => { + const parser = new Parser(); + let feed = await parser.parseURL("https://forgejo.jk-effects.cloud/Ehrenamt/ff-operation-server/releases.rss"); + res.json(feed); +}); + +router.get("/clientrss", async (req: Request, res: Response) => { + const parser = new Parser(); + let feed = await parser.parseURL("https://forgejo.jk-effects.cloud/Ehrenamt/ff-operation/releases.rss"); + res.json(feed); +}); + +export default router; diff --git a/src/routes/setup.ts b/src/routes/setup.ts new file mode 100644 index 0000000..159e04e --- /dev/null +++ b/src/routes/setup.ts @@ -0,0 +1,28 @@ +import express from "express"; +import { isSetup } from "../controller/setupController"; +import { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController"; +import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper"; + +var router = express.Router({ mergeParams: true }); + +router.get("/", async (req, res) => { + await isSetup(req, res); +}); + +router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => { + await verifyInvite(req, res); +}); + +router.post( + "/", + ParamaterPassCheckHelper.requiredIncludedMiddleware(["username", "mail", "firstname", "lastname"]), + async (req, res) => { + await inviteUser(req, res, false); + } +); + +router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => { + await finishInvite(req, res, true); +}); + +export default router; diff --git a/src/routes/user.ts b/src/routes/user.ts new file mode 100644 index 0000000..d196e16 --- /dev/null +++ b/src/routes/user.ts @@ -0,0 +1,26 @@ +import express from "express"; +import { getMeById, getMyTotp, transferOwnership, updateMe, verifyMyTotp } from "../controller/userController"; + +var router = express.Router({ mergeParams: true }); + +router.get("/me", async (req, res) => { + await getMeById(req, res); +}); + +router.get("/totp", async (req, res) => { + await getMyTotp(req, res); +}); + +router.post("/verify", async (req, res) => { + await verifyMyTotp(req, res); +}); + +router.put("/transferOwner", async (req, res) => { + await transferOwnership(req, res); +}); + +router.patch("/me", async (req, res) => { + await updateMe(req, res); +}); + +export default router; diff --git a/src/service/configuration/memberService.ts b/src/service/configuration/memberService.ts new file mode 100644 index 0000000..e8eddc6 --- /dev/null +++ b/src/service/configuration/memberService.ts @@ -0,0 +1,80 @@ +import { dataSource } from "../../data-source"; +import { member } from "../../entity/configuration/member"; +import DatabaseActionException from "../../exceptions/databaseActionException"; + +export default abstract class MemberService { + /** + * @description get all members + * @returns {Promise<[Array, number]>} + */ + static async getAll({ + offset = 0, + count = 25, + search = "", + noLimit = false, + ids = [], + }: { + offset?: number; + count?: number; + search?: string; + noLimit?: boolean; + ids?: Array; + }): Promise<[Array, number]> { + let query = dataSource.getRepository(member).createQueryBuilder("member"); + + if (search != "") { + search.split(" ").forEach((term, index) => { + const searchQuery = `%${term}%`; + const dynamic = "searchQuery" + Math.random().toString(36).substring(2); + if (index == 0) { + query = query.where(`member.firstname LIKE :${dynamic} OR member.lastname LIKE :${dynamic}`, { + [dynamic]: searchQuery, + }); + } else { + query = query.orWhere(`member.firstname LIKE :${dynamic} OR member.lastname LIKE :${dynamic}`, { + [dynamic]: searchQuery, + }); + } + }); + } + + if (ids.length != 0) { + query = query.where("member.id IN (:...ids)", { ids: ids }); + } + + if (!noLimit) { + query = query.offset(offset).limit(count); + } + + return await query + .orderBy("member.lastname") + .addOrderBy("member.firstname") + .addOrderBy("member.nameaffix") + .getManyAndCount() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "member", err); + }); + } + + /** + * @description get member by id + * @param {string} id + * @returns {Promise} + */ + static async getById(id: string): Promise { + return dataSource + .getRepository(member) + .createQueryBuilder("member") + .where("member.id = :id", { id: id }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "member", err); + }); + } +} diff --git a/src/service/management/inviteService.ts b/src/service/management/inviteService.ts new file mode 100644 index 0000000..573975a --- /dev/null +++ b/src/service/management/inviteService.ts @@ -0,0 +1,44 @@ +import { dataSource } from "../../data-source"; +import { invite } from "../../entity/management/invite"; +import DatabaseActionException from "../../exceptions/databaseActionException"; +import InternalException from "../../exceptions/internalException"; + +export default abstract class InviteService { + /** + * @description get all invites + * @returns {Promise>} + */ + static async getAll(): Promise> { + return await dataSource + .getRepository(invite) + .createQueryBuilder("invite") + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "invite", err); + }); + } + + /** + * @description get invite by id + * @param mail string + * @param token string + * @returns {Promise} + */ + static async getByMailAndToken(mail: string, token: string): Promise { + return await dataSource + .getRepository(invite) + .createQueryBuilder("invite") + .where("invite.mail = :mail", { mail: mail }) + .andWhere("invite.token = :token", { token: token }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "invite", err); + }); + } +} diff --git a/src/service/management/rolePermissionService.ts b/src/service/management/rolePermissionService.ts new file mode 100644 index 0000000..c96c73b --- /dev/null +++ b/src/service/management/rolePermissionService.ts @@ -0,0 +1,45 @@ +import { dataSource } from "../../data-source"; +import { rolePermission } from "../../entity/management/role_permission"; +import { userPermission } from "../../entity/management/user_permission"; +import DatabaseActionException from "../../exceptions/databaseActionException"; +import InternalException from "../../exceptions/internalException"; + +export default abstract class RolePermissionService { + /** + * @description get permission by role + * @param roleId number + * @returns {Promise>} + */ + static async getByRole(roleId: number): Promise> { + return await dataSource + .getRepository(rolePermission) + .createQueryBuilder("permission") + .where("permission.roleId = :roleId", { roleId: roleId }) + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "rolePermission", err); + }); + } + + /** + * @description get permission by roles + * @param roleIds Array + * @returns {Promise>} + */ + static async getByRoles(roleIds: Array): Promise> { + return await dataSource + .getRepository(rolePermission) + .createQueryBuilder("permission") + .where("permission.roleId IN (:...roleIds)", { roleIds: roleIds }) + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "rolePermission", err); + }); + } +} diff --git a/src/service/management/roleService.ts b/src/service/management/roleService.ts new file mode 100644 index 0000000..c3a2dcd --- /dev/null +++ b/src/service/management/roleService.ts @@ -0,0 +1,45 @@ +import { dataSource } from "../../data-source"; +import { role } from "../../entity/management/role"; +import DatabaseActionException from "../../exceptions/databaseActionException"; +import InternalException from "../../exceptions/internalException"; + +export default abstract class RoleService { + /** + * @description get roles + * @returns {Promise>} + */ + static async getAll(): Promise> { + return await dataSource + .getRepository(role) + .createQueryBuilder("role") + .leftJoinAndSelect("role.permissions", "role_permissions") + .orderBy("role", "ASC") + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "roles", err); + }); + } + + /** + * @description get role by id + * @param id number + * @returns {Promise} + */ + static async getById(id: number): Promise { + return await dataSource + .getRepository(role) + .createQueryBuilder("role") + .leftJoinAndSelect("role.permissions", "role_permissions") + .where("role.id = :id", { id: id }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "role", err); + }); + } +} diff --git a/src/service/management/userPermissionService.ts b/src/service/management/userPermissionService.ts new file mode 100644 index 0000000..0be67e0 --- /dev/null +++ b/src/service/management/userPermissionService.ts @@ -0,0 +1,25 @@ +import { dataSource } from "../../data-source"; +import { userPermission } from "../../entity/management/user_permission"; +import DatabaseActionException from "../../exceptions/databaseActionException"; +import InternalException from "../../exceptions/internalException"; + +export default abstract class UserPermissionService { + /** + * @description get permission by user + * @param userId string + * @returns {Promise>} + */ + static async getByUser(userId: string): Promise> { + return await dataSource + .getRepository(userPermission) + .createQueryBuilder("permission") + .where({ userId: userId }) + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "userPermission", err); + }); + } +} diff --git a/src/service/management/userService.ts b/src/service/management/userService.ts new file mode 100644 index 0000000..8fe4dd4 --- /dev/null +++ b/src/service/management/userService.ts @@ -0,0 +1,132 @@ +import { dataSource } from "../../data-source"; +import { role } from "../../entity/management/role"; +import { user } from "../../entity/management/user"; +import DatabaseActionException from "../../exceptions/databaseActionException"; +import InternalException from "../../exceptions/internalException"; + +export default abstract class UserService { + /** + * @description get users + * @returns {Promise>} + */ + static async getAll(): Promise> { + return await dataSource + .getRepository(user) + .createQueryBuilder("user") + .leftJoinAndSelect("user.roles", "roles") + .leftJoinAndSelect("user.permissions", "permissions") + .leftJoinAndSelect("roles.permissions", "role_permissions") + .orderBy("firstname", "ASC") + .addOrderBy("lastname", "ASC") + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "user", err); + }); + } + + /** + * @description get user by id + * @param id string + * @returns {Promise} + */ + static async getById(id: string): Promise { + return await dataSource + .getRepository(user) + .createQueryBuilder("user") + .leftJoinAndSelect("user.roles", "roles") + .leftJoinAndSelect("user.permissions", "permissions") + .leftJoinAndSelect("roles.permissions", "role_permissions") + .where("user.id = :id", { id: id }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "user", err); + }); + } + + /** + * @description get user by username + * @param username string + * @returns {Promise} + */ + static async getByUsername(username: string): Promise { + return await dataSource + .getRepository(user) + .createQueryBuilder("user") + .select() + .where("user.username = :username", { username: username }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "user", err); + }); + } + + /** + * @description get users by mail or username + * @param username string + * @param mail string + * @returns {Promise>} + */ + static async getByMailOrUsername(mail?: string, username?: string): Promise> { + return await dataSource + .getRepository(user) + .createQueryBuilder("user") + .select() + .where("user.mail = :mail", { mail: mail }) + .orWhere("user.username = :username", { username: username }) + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "user", err); + }); + } + + /** + * @description get count of users + * @returns {Promise} + */ + static async count(): Promise { + return await dataSource + .getRepository(user) + .createQueryBuilder("user") + .select() + .getCount() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("COUNT", "users", err); + }); + } + + /** + * @description get roles assigned to user + * @param userId string + * @returns {Promise>} + */ + static async getAssignedRolesByUserId(userId: string): Promise> { + return await dataSource + .getRepository(user) + .createQueryBuilder("user") + .leftJoinAndSelect("user.roles", "roles") + .leftJoinAndSelect("roles.permissions", "role_permissions") + .where("user.id = :id", { id: userId }) + .getOneOrFail() + .then((res) => { + return res.roles; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "userRoles", err); + }); + } +} diff --git a/src/service/refreshService.ts b/src/service/refreshService.ts new file mode 100644 index 0000000..a2b8a2c --- /dev/null +++ b/src/service/refreshService.ts @@ -0,0 +1,26 @@ +import { dataSource } from "../data-source"; +import { refresh } from "../entity/refresh"; +import InternalException from "../exceptions/internalException"; + +export default abstract class RefreshService { + /** + * @description get refresh by token + * @param token string + * @returns {Promise} + */ + static async getByToken(token: string): Promise { + return await dataSource + .getRepository(refresh) + .createQueryBuilder("refresh") + .leftJoinAndSelect("refresh.user", "user") + .where("refresh.token = :token", { token: token }) + .andWhere("refresh.expiry >= :expiry", { expiry: new Date() }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("refresh not found", err); + }); + } +} diff --git a/src/service/resetService.ts b/src/service/resetService.ts new file mode 100644 index 0000000..71a3c99 --- /dev/null +++ b/src/service/resetService.ts @@ -0,0 +1,27 @@ +import { dataSource } from "../data-source"; +import { reset } from "../entity/reset"; +import DatabaseActionException from "../exceptions/databaseActionException"; +import InternalException from "../exceptions/internalException"; + +export default abstract class ResetService { + /** + * @description get reset by id + * @param mail string + * @param token string + * @returns {Promise} + */ + static async getByMailAndToken(mail: string, token: string): Promise { + return await dataSource + .getRepository(reset) + .createQueryBuilder("reset") + .where("reset.mail = :mail", { mail: mail }) + .andWhere("reset.token = :token", { token: token }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "reset", err); + }); + } +} diff --git a/src/type/jwtTypes.ts b/src/type/jwtTypes.ts new file mode 100644 index 0000000..6a0486d --- /dev/null +++ b/src/type/jwtTypes.ts @@ -0,0 +1,19 @@ +import { PermissionObject } from "./permissionTypes"; + +export type JWTData = { + [key: string]: string | number | boolean | PermissionObject; +}; + +export type JWTToken = { + userId: string; + mail: string; + username: string; + firstname: string; + lastname: string; + isOwner: boolean; + permissions: PermissionObject; +} & JWTData; + +export type JWTRefresh = { + userId: number; +} & JWTData; diff --git a/src/type/permissionTypes.ts b/src/type/permissionTypes.ts new file mode 100644 index 0000000..5581de5 --- /dev/null +++ b/src/type/permissionTypes.ts @@ -0,0 +1,33 @@ +export type PermissionSection = "operation" | "configuration" | "management"; + +export type PermissionModule = "mission" | "force" | "user" | "role" | "backup"; + +export type PermissionType = "read" | "create" | "update" | "delete"; + +export type PermissionString = + | `${PermissionSection}.${PermissionModule}.${PermissionType}` // für spezifische Berechtigungen + | `${PermissionSection}.${PermissionModule}.*` // für alle Berechtigungen in einem Modul + | `${PermissionSection}.${PermissionType}` // für spezifische Berechtigungen in einem Abschnitt + | `${PermissionSection}.*` // für alle Berechtigungen in einem Abschnitt + | "*"; // für Admin + +export type PermissionObject = { + [section in PermissionSection]?: { + [module in PermissionModule]?: Array | "*"; + } & { all?: Array | "*" }; +} & { + admin?: boolean; +}; + +export type SectionsAndModulesObject = { + [section in PermissionSection]: Array; +}; + +export const permissionSections: Array = ["operation", "configuration", "management"]; +export const permissionModules: Array = ["mission", "force", "user", "role", "backup"]; +export const permissionTypes: Array = ["read", "create", "update", "delete"]; +export const sectionsAndModules: SectionsAndModulesObject = { + operation: ["mission"], + configuration: ["force"], + management: ["user", "role", "backup"], +}; diff --git a/src/viewmodel/admin/configuration/member.models.ts b/src/viewmodel/admin/configuration/member.models.ts new file mode 100644 index 0000000..f06b8ba --- /dev/null +++ b/src/viewmodel/admin/configuration/member.models.ts @@ -0,0 +1,6 @@ +export interface MemberViewModel { + id: string; + firstname: string; + lastname: string; + nameaffix: string; +} diff --git a/src/viewmodel/admin/management/invite.models.ts b/src/viewmodel/admin/management/invite.models.ts new file mode 100644 index 0000000..6f886df --- /dev/null +++ b/src/viewmodel/admin/management/invite.models.ts @@ -0,0 +1,6 @@ +export interface InviteViewModel { + mail: string; + username: string; + firstname: string; + lastname: string; +} diff --git a/src/viewmodel/admin/management/role.models.ts b/src/viewmodel/admin/management/role.models.ts new file mode 100644 index 0000000..ba0e76a --- /dev/null +++ b/src/viewmodel/admin/management/role.models.ts @@ -0,0 +1,7 @@ +import { PermissionObject } from "../../../type/permissionTypes"; + +export interface RoleViewModel { + id: number; + permissions: PermissionObject; + role: string; +} diff --git a/src/viewmodel/admin/management/user.models.ts b/src/viewmodel/admin/management/user.models.ts new file mode 100644 index 0000000..b9c5984 --- /dev/null +++ b/src/viewmodel/admin/management/user.models.ts @@ -0,0 +1,14 @@ +import { PermissionObject } from "../../../type/permissionTypes"; +import { RoleViewModel } from "./role.models"; + +export interface UserViewModel { + id: string; + username: string; + mail: string; + firstname: string; + lastname: string; + isOwner: boolean; + permissions: PermissionObject; + roles: Array; + permissions_total: PermissionObject; +} diff --git a/src/viewmodel/permissionViewModel.ts b/src/viewmodel/permissionViewModel.ts new file mode 100644 index 0000000..7a7f8fb --- /dev/null +++ b/src/viewmodel/permissionViewModel.ts @@ -0,0 +1 @@ +export interface PermissionViewModel {} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5645278 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "module": "commonjs", + "esModuleInterop": true, + "target": "esnext", + "noImplicitAny": true, + "skipLibCheck": true, + "moduleResolution": "node", + "sourceMap": true, + "outDir": "dist", + "baseUrl": ".", + "paths": { + "*": ["node_modules/*"] + } + }, + "include": ["src/**/*"] +}