From f01973f9c1f2a3eb56938ea536f574be12d95b84 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 12 Jan 2024 14:03:27 +0100 Subject: [PATCH 001/206] [Task] #6 provide fallback index.html --- httpdocs/index.html | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 httpdocs/index.html diff --git a/httpdocs/index.html b/httpdocs/index.html new file mode 100644 index 0000000..9a20fce --- /dev/null +++ b/httpdocs/index.html @@ -0,0 +1,52 @@ + + + + + + Welcome Page + + + +

Welcome

+ + From c19508ff0afd2dc9d8bb73ed69c5ef6d36b07871 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 12 Jan 2024 14:47:11 +0100 Subject: [PATCH 002/206] [Task] #6 production ready code (m) move httpdocs folder to dist have compile without sourcemaps for faster speed --- .github/workflows/build.yml | 2 +- package.json | 5 ++++- tsconfig.prod.json | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 tsconfig.prod.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 097687e..57fe368 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: - run: node --version - uses: actions/checkout@v3 - run: npm ci - - run: npm run build --if-present + - run: npm run build:prod --if-present - name: Start server and test server response run: | npm start & diff --git a/package.json b/package.json index c555c50..7d9bdd9 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "index.js", "type": "module", "scripts": { - "build": "npx tsc", + "build": "npx tsc && cp -R httpdocs/ dist/", + "build:prod": "npx tsc -p ./tsconfig.prod.json && cp -R httpdocs/ dist/", "start": "node dist/app.js", "dev": "nodemon src/app.ts", "lint": "eslint . --fix" @@ -27,3 +28,5 @@ "express": "^4.18.2" } } + + diff --git a/tsconfig.prod.json b/tsconfig.prod.json new file mode 100644 index 0000000..513a96c --- /dev/null +++ b/tsconfig.prod.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false + } +} \ No newline at end of file From e09f9789eb032c1220b7fca8af1e9509148cef73 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 12 Jan 2024 15:41:34 +0100 Subject: [PATCH 003/206] [Task] #6 create github action for upload when main is updated (#21) --- .github/workflows/ftp.yml | 25 +++++++++++++++++++++++++ .github/workflows/label.yml | 22 ---------------------- 2 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/ftp.yml delete mode 100644 .github/workflows/label.yml diff --git a/.github/workflows/ftp.yml b/.github/workflows/ftp.yml new file mode 100644 index 0000000..b379796 --- /dev/null +++ b/.github/workflows/ftp.yml @@ -0,0 +1,25 @@ +name: Deploy via ftp +on: + push: + branches: [ "main" ] +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 20 + - name: Install dependencies + run: npm i + - name: Build + run: npm run build:prod + - name: Upload ftp + uses: GenieTim/ftp-action@v4.0.1 + with: + host: ${{ secrets.FTP_SERVER }} + user: ${{ secrets.FTP_USERNAME }} + password: ${{ secrets.FTP_PASSWORD }} + localDir: "dist" diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml deleted file mode 100644 index 4613569..0000000 --- a/.github/workflows/label.yml +++ /dev/null @@ -1,22 +0,0 @@ -# This workflow will triage pull requests and apply a label based on the -# paths that are modified in the pull request. -# -# To use this workflow, you will need to set up a .github/labeler.yml -# file with configuration. For more information, see: -# https://github.com/actions/labeler - -name: Labeler -on: [pull_request_target] - -jobs: - label: - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - - steps: - - uses: actions/labeler@v4 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" From 9c2a2fce30d47758629a36c7ac827bb27d2e869f Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 12 Jan 2024 15:57:41 +0100 Subject: [PATCH 004/206] [change] #6 new ftp upload action --- .github/workflows/ftp.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ftp.yml b/.github/workflows/ftp.yml index b379796..01aa3d6 100644 --- a/.github/workflows/ftp.yml +++ b/.github/workflows/ftp.yml @@ -17,9 +17,9 @@ jobs: - name: Build run: npm run build:prod - name: Upload ftp - uses: GenieTim/ftp-action@v4.0.1 + uses: airvzxf/ftp-deployment-action@latest with: host: ${{ secrets.FTP_SERVER }} user: ${{ secrets.FTP_USERNAME }} password: ${{ secrets.FTP_PASSWORD }} - localDir: "dist" + local_dir: "dist" From bae1b4e379595c8696f6d789101e2722e939c8cc Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 12 Jan 2024 16:13:30 +0100 Subject: [PATCH 005/206] [Fix] #6 replace host with server in ftp action --- .github/workflows/ftp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ftp.yml b/.github/workflows/ftp.yml index 01aa3d6..eb615d8 100644 --- a/.github/workflows/ftp.yml +++ b/.github/workflows/ftp.yml @@ -19,7 +19,7 @@ jobs: - name: Upload ftp uses: airvzxf/ftp-deployment-action@latest with: - host: ${{ secrets.FTP_SERVER }} + server: ${{ secrets.FTP_SERVER }} user: ${{ secrets.FTP_USERNAME }} password: ${{ secrets.FTP_PASSWORD }} local_dir: "dist" From 861336bef72cd39a8fed89576ce1f44f84e10f93 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 12 Jan 2024 17:14:37 +0100 Subject: [PATCH 006/206] [Task] #6 basic log (#26) --- package.json | 5 +++-- src/app.ts | 14 +++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7d9bdd9..c485f04 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "main": "index.js", "type": "module", "scripts": { - "build": "npx tsc && cp -R httpdocs/ dist/", + "clean": "rm -rf dist/*", + "build": "rm -rf dist/* && npx tsc && cp -R httpdocs/ dist/", "build:prod": "npx tsc -p ./tsconfig.prod.json && cp -R httpdocs/ dist/", "start": "node dist/app.js", - "dev": "nodemon src/app.ts", + "dev": "rm -rf dist/* && cp -R httpdocs/ dist/ && nodemon src/app.ts", "lint": "eslint . --fix" }, "keywords": [], diff --git a/src/app.ts b/src/app.ts index 9aafa27..a2daf09 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,14 +1,22 @@ import express from 'express'; import { Request, Response } from 'express'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); const app = express(); const port = 80; app.get('/', (req: Request, res: Response) => { - res.send('Hello World, via TypeScript and Node.js!'); - + res.send('Hello World, via TypeScript and Node.js!'); }); app.listen(port, () => { - console.log(`Server running at http://localhost:${port}`); + const date = new Date().toLocaleString('de-DE', { hour12: false }); + const logPath = join(__dirname, 'httpdocs', 'log.txt'); + fs.appendFileSync(logPath, `Express: Server: ${date} \n`); + console.log(`Server läuft unter http://localhost:${port}`); }); \ No newline at end of file From 63b352543670021a7ba02e47fd0b0853c663e8e1 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 12 Jan 2024 18:32:29 +0100 Subject: [PATCH 007/206] [CHANGE] #6 revert back to require output for production --- package.json | 1 - src/app.ts | 11 ++++------- tsconfig.json | 12 ++++++------ 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index c485f04..166a51b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "0.0.1", "description": "Leaflet Osmand React ExpressJS Coordinates (X)\r > Remember an \"X\" marks the spot.", "main": "index.js", - "type": "module", "scripts": { "clean": "rm -rf dist/*", "build": "rm -rf dist/* && npx tsc && cp -R httpdocs/ dist/", diff --git a/src/app.ts b/src/app.ts index a2daf09..9537cc7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,11 +1,7 @@ import express from 'express'; import { Request, Response } from 'express'; import fs from 'fs'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +import path from 'path'; const app = express(); const port = 80; @@ -16,7 +12,8 @@ app.get('/', (req: Request, res: Response) => { app.listen(port, () => { const date = new Date().toLocaleString('de-DE', { hour12: false }); - const logPath = join(__dirname, 'httpdocs', 'log.txt'); + const logPath = path.join(__dirname, 'httpdocs', 'log.txt'); fs.appendFileSync(logPath, `Express: Server: ${date} \n`); + console.log(`Server läuft unter http://localhost:${port}`); -}); \ No newline at end of file +}); diff --git a/tsconfig.json b/tsconfig.json index 7a568e3..19cde21 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,11 +3,11 @@ "include": ["src/**/*"], "exclude": ["node_modules"], "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "target": "ES2022", - "sourceMap": true + "rootDir": "src", + "outDir": "dist", + "module": "CommonJS", + "moduleResolution": "node", + "target": "ES6", + "sourceMap": true } } \ No newline at end of file From 95aec15a0eb212ea2b3a6bb351f1dfc8802b5022 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 14 Jan 2024 22:05:33 +0100 Subject: [PATCH 008/206] [Task] #6 add ability to manually upload to prod --- .github/workflows/ftp.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ftp.yml b/.github/workflows/ftp.yml index eb615d8..7d20652 100644 --- a/.github/workflows/ftp.yml +++ b/.github/workflows/ftp.yml @@ -1,5 +1,6 @@ name: Deploy via ftp on: + workflow_dispatch: push: branches: [ "main" ] jobs: From 81cac9222d92b6ac5ad8869b0517313732b7bfff Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Jan 2024 16:34:18 +0100 Subject: [PATCH 009/206] [Task] #9 enable manual start of codechecks --- .github/workflows/codeql.yml | 1 + .github/workflows/njsscan.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 09665bd..dacf974 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -12,6 +12,7 @@ name: "CodeQL" on: + workflow_dispatch: push: branches: [ "dev" ] pull_request: diff --git a/.github/workflows/njsscan.yml b/.github/workflows/njsscan.yml index 77d882c..5295ead 100644 --- a/.github/workflows/njsscan.yml +++ b/.github/workflows/njsscan.yml @@ -9,6 +9,7 @@ name: njsscan sarif on: + workflow_dispatch: push: branches: [ "dev" ] pull_request: From 3e1fbbd57e29440fd61c9945b9ee9c562626522b Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 24 Jan 2024 15:09:24 +0100 Subject: [PATCH 010/206] 10 webhook for writing (#36) * [Change] #3 clean up npm scripts, to have clean folder before build * [Task] #10 created data types in typescript * [Temp] #10 created subroute for writing, and folder structure * [Change] #3 include to use relative paths from src folder in ts and node https://stackoverflow.com/questions/43281741/how-can-i-use-paths-in-tsconfig-json See comment from Remo H. Hansen with at least 100 upvoted * [Change] Update VSCode to keep files open * [Task] #18 setup dotenv for secret variables * [Temp, Task] #10 Validate inputs using express-validator and custom functions * [Task] #18 prevent parameter pollution * [Task] #10 validating incoming parameter and logging errors * [Task] #7 add basic cache to express * [Changes] #7 Error Handling, to include basic custom Error Handling * [Task] #10 enhanced validation to only allow known parameters * [Change] #35 added Jest, tests for helper functions when writing * [Task] #10 better error Handling * [Task] #35 add tests for writing webhook validation * [TASK] #18 protect Webhook using KEY * [Fix] #35 test know import path structure now * [Task] #35 add test for protected webhook * [Task] #35 refactor build to run jest tests * [Task] #10 switched to crypto instead of bcrypt for dependency issue see synk inflight * [Fix] #36 PRQ Feedback --- .eslintrc.json | 20 +- .github/workflows/build.yml | 9 +- .vscode/settings.json | 4 + jest.config.js | 9 + package-lock.json | 6004 +++++++++++++++++++++++++++------- package.json | 26 +- src/app.test.ts | 15 + src/app.ts | 37 +- src/cache.ts | 15 + src/controller/write.test.ts | 100 + src/controller/write.ts | 37 + src/error.ts | 32 + src/models/entry.test.ts | 47 + src/models/entry.ts | 91 + src/scripts/crypt.ts | 9 + src/scripts/logger.ts | 17 + src/scripts/time.ts | 0 tsconfig.json | 11 +- types.d.ts | 98 + 19 files changed, 5339 insertions(+), 1242 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 jest.config.js create mode 100644 src/app.test.ts create mode 100644 src/cache.ts create mode 100644 src/controller/write.test.ts create mode 100644 src/controller/write.ts create mode 100644 src/error.ts create mode 100644 src/models/entry.test.ts create mode 100644 src/models/entry.ts create mode 100644 src/scripts/crypt.ts create mode 100644 src/scripts/logger.ts create mode 100644 src/scripts/time.ts create mode 100644 types.d.ts diff --git a/.eslintrc.json b/.eslintrc.json index 1f419d8..30be6b6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,14 +6,22 @@ "sourceType": "module", "project": "tsconfig.json" }, - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:jest/recommended" + ], "env": { - "node": true + "node": true, + "jest/globals": true }, + "plugins": ["jest", "@typescript-eslint"], "rules": { - //'no-console': 'off', - //'import/prefer-default-export': 'off', - //'@typescript-eslint/no-unused-vars': 'warn', + //"no-console": "off", + //"import/prefer-default-export": "off", + //"@typescript-eslint/no-unused-vars": "warn" + "jest/no-conditional-expect": "off" }, - "ignorePatterns": ["dist"] + "ignorePatterns": ["dist", "jest.config.js"] + } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57fe368..4d0f1e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,11 +18,8 @@ jobs: - uses: actions/checkout@v3 - run: npm ci - run: npm run build:prod --if-present - - name: Start server and test server response + - name: Start server run: | npm start & - sleep 5 # Wait for server to start - curl localhost:80 - if [ -n "$(jobs -p)" ]; then - kill $(jobs -p) # Kill background jobs - fi + sleep 5 # Give server some time to start + - run: npm test diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..700d626 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "workbench.editor.enablePreview": false, + "editor.rename.enablePreview": false +} \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..2adbbae --- /dev/null +++ b/jest.config.js @@ -0,0 +1,9 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + modulePathIgnorePatterns: ['/dist/'], + moduleNameMapper: { + '^@src/(.*)$': '/src/$1', + }, +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ea8f284..808a918 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,28 @@ "name": "lorex", "version": "0.0.1", "dependencies": { - "express": "^4.18.2" + "express": "^4.18.2", + "express-validator": "^7.0.1", + "hpp": "^0.2.3", + "module-alias": "^2.2.3" }, "devDependencies": { + "@jest/globals": "^29.7.0", "@tsconfig/node20": "^20.1.2", + "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.21", + "@types/hpp": "^0.2.5", + "@types/jest": "^29.5.11", "@types/node": "^20.10.6", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", + "axios": "^1.6.5", + "dotenv": "^16.3.1", "eslint": "^8.56.0", + "eslint-plugin-jest": "^27.6.3", + "jest": "^29.7.0", "nodemon": "^3.0.2", + "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "typescript": "^5.3.3" } @@ -31,66 +43,122 @@ "node": ">=0.10.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { - "node": ">=12" + "node": ">=6.0.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=4" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", + "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@eslint/eslintrc/node_modules/debug": { + "node_modules/@babel/core/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", @@ -107,503 +175,514 @@ } } }, - "node_modules/@eslint/eslintrc/node_modules/ms": { + "node_modules/@babel/core/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==", "dev": true }, - "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" }, "engines": { - "node": ">=10.10.0" + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "dependencies": { - "ms": "2.1.2" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/config-array/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==", - "dev": true + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dev": true, "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@babel/types": "^7.22.15" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@babel/types": "^7.22.5" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } }, - "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==", - "dev": true + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "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==", - "dev": true + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "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==", - "dev": true + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@tsconfig/node20": { - "version": "20.1.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.2.tgz", - "integrity": "sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==", - "dev": true + "node_modules/@babel/helpers": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz", + "integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + }, + "engines": { + "node": ">=6.9.0" + } }, - "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==", + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { - "@types/connect": "*", - "@types/node": "*" + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { - "@types/node": "*" + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" } }, - "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==", + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.41", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", - "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" + "color-name": "1.1.3" } }, - "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==", + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } }, - "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/@babel/parser": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } }, - "node_modules/@types/node": { - "version": "20.10.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", - "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@types/qs": { - "version": "6.9.11", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", - "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", - "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/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", - "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==", + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, "dependencies": { - "@types/mime": "^1", - "@types/node": "*" + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@types/serve-static": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", - "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "dependencies": { - "@types/http-errors": "*", - "@types/mime": "*", - "@types/node": "*" + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.18.1.tgz", - "integrity": "sha512-nISDRYnnIpk7VCFrGcu1rnZfM1Dh9LRHnfgdkjcbi/l7g16VYRri3TjXi9Ir4lOZSw5N/gnV/3H7jIPQ8Q4daA==", + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.18.1", - "@typescript-eslint/type-utils": "6.18.1", - "@typescript-eslint/utils": "6.18.1", - "@typescript-eslint/visitor-keys": "6.18.1", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/eslint-plugin/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==", - "dev": true - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.18.1.tgz", - "integrity": "sha512-zct/MdJnVaRRNy9e84XnVtRv9Vf91/qqe+hZJtKanjojud4wAVy/7lXxJmMyX6X6J+xc6c//YEWvpeif8cAhWA==", + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.18.1", - "@typescript-eslint/types": "6.18.1", - "@typescript-eslint/typescript-estree": "6.18.1", - "@typescript-eslint/visitor-keys": "6.18.1", - "debug": "^4.3.4" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=6.9.0" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/parser/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" + "@babel/helper-plugin-utils": "^7.10.4" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/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==", - "dev": true - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.1.tgz", - "integrity": "sha512-BgdBwXPFmZzaZUuw6wKiHKIovms97a7eTImjkXCZE04TGHysG+0hDQPmygyvgtkoB/aOQwSM/nWv3LzrOIQOBw==", + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.18.1", - "@typescript-eslint/visitor-keys": "6.18.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.18.1.tgz", - "integrity": "sha512-wyOSKhuzHeU/5pcRDP2G2Ndci+4g653V43gXTpt4nbyoIOAASkGDA9JIAgbQCdCkcr1MvpSYWzxTz0olCn8+/Q==", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.18.1", - "@typescript-eslint/utils": "6.18.1", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "@babel/helper-plugin-utils": "^7.10.4" }, - "engines": { - "node": "^16.0.0 || >=18.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "dependencies": { - "ms": "2.1.2" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=6.0" + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/type-utils/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==", - "dev": true - }, - "node_modules/@typescript-eslint/types": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.1.tgz", - "integrity": "sha512-4TuMAe+tc5oA7wwfqMtB0Y5OrREPF1GeJBAjqwgZh1lEMH5PJQgWgHGfYufVB51LtjD+peZylmeyxUXPfENLCw==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": ">=6.9.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.1.tgz", - "integrity": "sha512-fv9B94UAhywPRhUeeV/v+3SBDvcPiLxRZJw/xZeeGgRLQZ6rLMG+8krrJUyIf6s1ecWTzlsbp0rlw7n9sjufHA==", + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.18.1", - "@typescript-eslint/visitor-keys": "6.18.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=6.9.0" } }, - "node_modules/@typescript-eslint/typescript-estree/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==", + "node_modules/@babel/traverse": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "node_modules/@babel/traverse/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", @@ -620,513 +699,2147 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=4" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "node_modules/@babel/traverse/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==", "dev": true }, - "node_modules/@typescript-eslint/utils": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.18.1.tgz", - "integrity": "sha512-zZmTuVZvD1wpoceHvoQpOiewmWu3uP9FuTWo8vqpy2ffsmfCE8mklRPi+vmnIYAIk9t/4kOThri2QCDgor+OpQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.18.1", - "@typescript-eslint/types": "6.18.1", - "@typescript-eslint/typescript-estree": "6.18.1", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.1.tgz", - "integrity": "sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==", + "node_modules/@babel/types": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.18.1", - "eslint-visitor-keys": "^3.4.1" + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=6.9.0" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "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==", + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": 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==", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { - "node": ">= 0.6" + "node": ">=12" } }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "eslint-visitor-keys": "^3.3.0" }, "engines": { - "node": ">=0.4.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", - "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=0.4.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "ms": "2.1.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "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==", + "node_modules/@eslint/eslintrc/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==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { - "node": ">=8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "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==", + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=10.10.0" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "ms": "2.1.2" }, "engines": { - "node": ">= 8" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "node_modules/@humanwhocodes/config-array/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==", "dev": true }, - "node_modules/argparse": { + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, "engines": { "node": ">=8" } }, - "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==", - "dev": true + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "node_modules/@istanbuljs/load-nyc-config/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==", "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "node_modules/@istanbuljs/load-nyc-config/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==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "p-locate": "^4.1.0" }, "engines": { "node": ">=8" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "node_modules/@istanbuljs/load-nyc-config/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==", + "dev": true, "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/@istanbuljs/load-nyc-config/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==", "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=8" } }, - "node_modules/chalk/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==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, "engines": { "node": ">=8" } }, - "node_modules/chalk/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==", + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "engines": { - "node": ">= 8.10.0" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "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==", + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" }, "engines": { - "node": ">=7.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.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==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": 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==", + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, "dependencies": { - "safe-buffer": "5.2.1" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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/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==", - "dev": true - }, - "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==", + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "engines": { - "node": ">= 8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, "dependencies": { - "ms": "2.0.0" + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } }, - "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">= 0.4" + "node": ">=10" } }, - "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==", + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, "engines": { - "node": ">= 0.8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, - "engines": { - "node": ">=0.3.1" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "dependencies": { - "path-type": "^4.0.0" + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "dependencies": { - "esutils": "^2.0.2" + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" }, "engines": { - "node": ">=6.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": 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==", + "dev": 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==", + "dev": 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==", + "dev": true + }, + "node_modules/@tsconfig/node20": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.2.tgz", + "integrity": "sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "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/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.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hpp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@types/hpp/-/hpp-0.2.5.tgz", + "integrity": "sha512-CBCYtHX0Gb2+jUs8BkLA4UbI/gBzsg93ffVPrVvrKQmXekcbQkxmwr//ccpHUdgVCD2IOaPsSJTXkCV6yeGaHg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "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/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "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/node": { + "version": "20.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", + "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/qs": { + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", + "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/semver": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "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.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.18.1.tgz", + "integrity": "sha512-nISDRYnnIpk7VCFrGcu1rnZfM1Dh9LRHnfgdkjcbi/l7g16VYRri3TjXi9Ir4lOZSw5N/gnV/3H7jIPQ8Q4daA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.18.1", + "@typescript-eslint/type-utils": "6.18.1", + "@typescript-eslint/utils": "6.18.1", + "@typescript-eslint/visitor-keys": "6.18.1", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/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==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.18.1.tgz", + "integrity": "sha512-zct/MdJnVaRRNy9e84XnVtRv9Vf91/qqe+hZJtKanjojud4wAVy/7lXxJmMyX6X6J+xc6c//YEWvpeif8cAhWA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.18.1", + "@typescript-eslint/types": "6.18.1", + "@typescript-eslint/typescript-estree": "6.18.1", + "@typescript-eslint/visitor-keys": "6.18.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/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==", + "dev": true + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.1.tgz", + "integrity": "sha512-BgdBwXPFmZzaZUuw6wKiHKIovms97a7eTImjkXCZE04TGHysG+0hDQPmygyvgtkoB/aOQwSM/nWv3LzrOIQOBw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.18.1", + "@typescript-eslint/visitor-keys": "6.18.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.18.1.tgz", + "integrity": "sha512-wyOSKhuzHeU/5pcRDP2G2Ndci+4g653V43gXTpt4nbyoIOAASkGDA9JIAgbQCdCkcr1MvpSYWzxTz0olCn8+/Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.18.1", + "@typescript-eslint/utils": "6.18.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/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==", + "dev": true + }, + "node_modules/@typescript-eslint/types": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.1.tgz", + "integrity": "sha512-4TuMAe+tc5oA7wwfqMtB0Y5OrREPF1GeJBAjqwgZh1lEMH5PJQgWgHGfYufVB51LtjD+peZylmeyxUXPfENLCw==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.1.tgz", + "integrity": "sha512-fv9B94UAhywPRhUeeV/v+3SBDvcPiLxRZJw/xZeeGgRLQZ6rLMG+8krrJUyIf6s1ecWTzlsbp0rlw7n9sjufHA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.18.1", + "@typescript-eslint/visitor-keys": "6.18.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/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==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.18.1.tgz", + "integrity": "sha512-zZmTuVZvD1wpoceHvoQpOiewmWu3uP9FuTWo8vqpy2ffsmfCE8mklRPi+vmnIYAIk9t/4kOThri2QCDgor+OpQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.18.1", + "@typescript-eslint/types": "6.18.1", + "@typescript-eslint/typescript-estree": "6.18.1", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.1.tgz", + "integrity": "sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.18.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "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==", + "dev": 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.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "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==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001579", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", + "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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/chalk/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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/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==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": 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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "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/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "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-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=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/electron-to-chromium": { + "version": "1.4.640", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.640.tgz", + "integrity": "sha512-z/6oZ/Muqk4BaE7P69bXhUhpJbUM9ZJeka43ZwxsDshKtePns4mhBlh8bU5+yrnOnz3fhG82XLzGUXazOmsWnA==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "dev": true }, "node_modules/encodeurl": { "version": "1.0.2", @@ -1136,16 +2849,830 @@ "node": ">= 0.8" } }, - "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/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "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/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "27.6.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.3.tgz", + "integrity": "sha512-+YsJFVH6R+tOiO3gCJon5oqn4KWc+mDq2leudk8mrp8RFubLOo9CVyi3cib4L7XMpxExmkmBZQTPDYVBzgpgOA==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-plugin-jest/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-jest/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==", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/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==", + "dev": true + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "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", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-validator": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", + "integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==", + "dependencies": { + "lodash": "^4.17.21", + "validator": "^13.9.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/escape-string-regexp": { + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": 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/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, "engines": { "node": ">=10" }, @@ -1153,691 +3680,1009 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", - "dev": true, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "get-intrinsic": "^1.1.3" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "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==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "get-intrinsic": "^1.2.2" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, + "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": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", "dependencies": { - "ms": "2.1.2" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=6.0" + "node": ">= 0.4" + } + }, + "node_modules/hpp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", + "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", + "dependencies": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "engines": { + "node": ">=0.10.0" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": 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/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dependencies": { - "is-glob": "^4.0.3" + "safer-buffer": ">= 2.1.2 < 3" }, "engines": { - "node": ">=10.13.0" + "node": ">=0.10.0" } }, - "node_modules/eslint/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/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "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==", + "dev": 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/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/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "dependencies": { - "estraverse": "^5.1.0" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "estraverse": "^5.2.0" + "hasown": "^2.0.0" }, - "engines": { - "node": ">=4.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "engines": { - "node": ">=4.0" + "node": ">=0.10.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "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==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "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", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">= 0.10.0" + "node": ">=0.10.0" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, "engines": { - "node": ">=8.6.0" + "node": ">=0.12.0" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, - "dependencies": { - "reusify": "^1.0.4" + "engines": { + "node": ">=8" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, "engines": { "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, "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" + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" }, "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "node_modules/istanbul-lib-report/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==", "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=8" } }, - "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/istanbul-lib-report/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==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, "engines": { - "node": ">= 0.6" + "node": ">=10" } }, - "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==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "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/istanbul-lib-source-maps/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==", + "dev": true }, - "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, "dependencies": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": 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" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": "*" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" }, "engines": { - "node": ">= 6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "dependencies": { - "type-fest": "^0.20.2" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.1.3" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, "engines": { - "node": ">=4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2" + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, "dependencies": { - "function-bind": "^1.1.2" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, "engines": { - "node": ">= 4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" }, "engines": { - "node": ">=6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, "engines": { - "node": ">=0.8.19" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": 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/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==", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, "engines": { - "node": ">= 0.10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "dependencies": { - "binary-extensions": "^2.0.0" + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, "engines": { - "node": ">=0.12.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "node_modules/jest-worker/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==", "dev": true, "engines": { "node": ">=8" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/js-yaml": { @@ -1852,12 +4697,30 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1870,6 +4733,18 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -1879,6 +4754,24 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1892,6 +4785,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1907,6 +4806,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1925,12 +4835,36 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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==", "dev": true }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1944,6 +4878,12 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2004,6 +4944,15 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2016,6 +4965,11 @@ "node": "*" } }, + "node_modules/module-alias": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -2035,6 +4989,18 @@ "node": ">= 0.6" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "node_modules/nodemon": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.2.tgz", @@ -2110,6 +5076,18 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -2138,6 +5116,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -2185,6 +5178,15 @@ "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==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2197,6 +5199,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2232,6 +5252,12 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -2243,28 +5269,146 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "engines": { - "node": ">=8.6" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, "engines": { - "node": ">= 0.8.0" + "node": ">= 6" } }, "node_modules/proxy-addr": { @@ -2279,6 +5423,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -2294,6 +5444,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -2350,6 +5516,12 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2362,6 +5534,53 @@ "node": ">=8.10.0" } }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2371,6 +5590,15 @@ "node": ">=4" } }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -2553,6 +5781,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "dev": true + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -2565,6 +5799,12 @@ "node": ">=10" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -2574,6 +5814,52 @@ "node": ">=8" } }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -2582,6 +5868,33 @@ "node": ">= 0.8" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "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", @@ -2594,6 +5907,24 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2618,12 +5949,53 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2668,6 +6040,49 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", + "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -2711,6 +6126,27 @@ } } }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2723,6 +6159,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -2780,6 +6225,36 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -2803,6 +6278,38 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2811,6 +6318,15 @@ "node": ">= 0.8" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2826,18 +6342,84 @@ "node": ">= 8" } }, + "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==", + "dev": true, + "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==", "dev": true }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "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/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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 166a51b..43db4ea 100644 --- a/package.json +++ b/package.json @@ -4,29 +4,43 @@ "description": "Leaflet Osmand React ExpressJS Coordinates (X)\r > Remember an \"X\" marks the spot.", "main": "index.js", "scripts": { - "clean": "rm -rf dist/*", - "build": "rm -rf dist/* && npx tsc && cp -R httpdocs/ dist/", + "prebuild": "rm -rf dist/*", + "build": "npx tsc && cp -R httpdocs/ dist/", "build:prod": "npx tsc -p ./tsconfig.prod.json && cp -R httpdocs/ dist/", "start": "node dist/app.js", "dev": "rm -rf dist/* && cp -R httpdocs/ dist/ && nodemon src/app.ts", - "lint": "eslint . --fix" + "lint": "eslint . --fix", + "test": "jest" }, "keywords": [], "author": "Type-Style", "devDependencies": { + "@jest/globals": "^29.7.0", "@tsconfig/node20": "^20.1.2", + "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.21", + "@types/hpp": "^0.2.5", + "@types/jest": "^29.5.11", "@types/node": "^20.10.6", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", + "axios": "^1.6.5", + "dotenv": "^16.3.1", "eslint": "^8.56.0", + "eslint-plugin-jest": "^27.6.3", + "jest": "^29.7.0", "nodemon": "^3.0.2", + "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "typescript": "^5.3.3" }, "dependencies": { - "express": "^4.18.2" + "express": "^4.18.2", + "express-validator": "^7.0.1", + "hpp": "^0.2.3", + "module-alias": "^2.2.3" + }, + "_moduleAliases": { + "@src": "dist" } } - - diff --git a/src/app.test.ts b/src/app.test.ts new file mode 100644 index 0000000..0682f2d --- /dev/null +++ b/src/app.test.ts @@ -0,0 +1,15 @@ +import axios from 'axios'; + +describe('Server Status', () => { + it('The server is running', async () => { + let serverStatus; + try { + const response = await axios.get('http://localhost'); + serverStatus = response.status; + } catch (error) { + console.error(error); + } + + expect(serverStatus).toBe(200); + }) +}) \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 9537cc7..382c255 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,19 +1,34 @@ +require('module-alias/register'); +import { config } from 'dotenv'; import express from 'express'; -import { Request, Response } from 'express'; -import fs from 'fs'; +import hpp from 'hpp'; +import cache from './cache'; +import * as error from "./error"; +import writeRouter from '@src/controller/write'; import path from 'path'; +import logger from '@src/scripts/logger'; + +// configurations +config(); const app = express(); -const port = 80; +app.use(hpp()); +app.use(cache); -app.get('/', (req: Request, res: Response) => { +// routes +app.get('/', (req, res) => { res.send('Hello World, via TypeScript and Node.js!'); }); +app.use('/write', writeRouter); -app.listen(port, () => { - const date = new Date().toLocaleString('de-DE', { hour12: false }); - const logPath = path.join(__dirname, 'httpdocs', 'log.txt'); - fs.appendFileSync(logPath, `Express: Server: ${date} \n`); - - console.log(`Server läuft unter http://localhost:${port}`); -}); +// use httpdocs as static folder +app.use('/', express.static(path.join(__dirname, 'httpdocs'))) + +// error handling +app.use(error.notFound); +app.use(error.handler); + +// init server +app.listen(80, () => { + logger.log(`Server running //localhost:80`); +}); \ No newline at end of file diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..c64ae4a --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,15 @@ +import {Request, Response, NextFunction } from 'express'; + +const setCache = function (req: Request, res: Response, next: NextFunction) { + const seconds = 60 * 5; // 5 minuits + + // cache get requests but nothing else + if (req.method == "GET") { + res.set("Cache-control", `public, max-age=${seconds}`); + } else { + res.set("Cache-control", 'no-store'); + } + + next(); +} +export default setCache; \ No newline at end of file diff --git a/src/controller/write.test.ts b/src/controller/write.test.ts new file mode 100644 index 0000000..f447d22 --- /dev/null +++ b/src/controller/write.test.ts @@ -0,0 +1,100 @@ +import axios, { AxiosError } from 'axios'; + +describe('HEAD /write', () => { + it('with all parameters correctly set it should succeed', async () => { + const timestamp = new Date().getTime(); + const response = await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); + expect(response.status).toBe(200); + }); + + it('without key it sends 403', async () => { + try { + const timestamp = new Date().getTime(); + await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0`); + } catch (error) { + const axiosError = error as AxiosError; + expect(axiosError.response!.status).toBe(403); + } + }); + + it('with user length not equal to 2 it sends 422', async () => { + try { + const timestamp = new Date().getTime(); + await axios.head(`http://localhost/write?user=x&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); + } catch (error) { + const axiosError = error as AxiosError; + expect(axiosError.response!.status).toBe(422); + } + }); + + + it('with lat not between -90 and 90 it sends 422', async () => { + try { + const timestamp = new Date().getTime(); + await axios.head(`http://localhost/write?user=xx&lat=91.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); + } catch (error) { + const axiosError = error as AxiosError; + expect(axiosError.response!.status).toBe(422); + } + }); + + it('with lon not between -180 and 180 it sends 422', async () => { + try { + const timestamp = new Date().getTime(); + await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=181.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); + } catch (error) { + const axiosError = error as AxiosError; + expect(axiosError.response!.status).toBe(422); + } + }); + + it('with timestamp to old sends 422', async () => { + try { + const timestamp = new Date().getTime() - 24 * 60 * 60 * 1000 * 2; // two days ago + await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); + } catch (error) { + const axiosError = error as AxiosError; + expect(axiosError.response!.status).toBe(422); + } + }) + + it('with hdop not between 0 and 100 it sends 422', async () => { + try { + const timestamp = new Date().getTime(); + await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); + } catch (error) { + const axiosError = error as AxiosError; + expect(axiosError.response!.status).toBe(422); + } + }); + + it('with altitude not between 0 and 10000 it sends 422', async () => { + try { + const timestamp = new Date().getTime(); + await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test`); + } catch (error) { + const axiosError = error as AxiosError; + expect(axiosError.response!.status).toBe(422); + } + }); + + it('with speed not between 0 and 300 it sends 422', async () => { + try { + const timestamp = new Date().getTime(); + await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test`); + } catch (error) { + const axiosError = error as AxiosError; + expect(axiosError.response!.status).toBe(422); + } + }); + + it('with heading not between 0 and 360 it sends 422', async () => { + try { + const timestamp = new Date().getTime(); + await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test`); + } catch (error) { + const axiosError = error as AxiosError; + expect(axiosError.response!.status).toBe(422); + } + }); +}); diff --git a/src/controller/write.ts b/src/controller/write.ts new file mode 100644 index 0000000..b62fd57 --- /dev/null +++ b/src/controller/write.ts @@ -0,0 +1,37 @@ +import express, { Request, Response, NextFunction } from 'express'; +import { entry } from '@src/models/entry'; +import { validationResult } from 'express-validator'; + +// example call: /write?user=xx&lat=00.000&lon=00.000×tamp=1704063600000&hdop=0.0&altitude=0.000&speed=0.000&heading=000.0 +function errorChecking (req:Request, res:Response, next:NextFunction) { + const errors = validationResult(req); + if (!errors.isEmpty()) { + const errorAsJson = { errors: errors.array()}; + const errorAsString = new Error(JSON.stringify(errorAsJson)); + const hasKeyErrors = errors.array().some(error => error.msg.includes("Key")); + + res.status(hasKeyErrors ? 403 : 422); // send forbidden or unprocessable content + return next(errorAsString); + } + + if (req.method == "HEAD") { + res.status(200).end(); + return; + } + + // Regular Save logic from here + + //entry.create(req, res); + //const test = process.env.TEST; + // res.send(req.query); + +} + + +const router = express.Router(); +router.use(entry.validate); + +router.get('/', errorChecking); +router.head('/', errorChecking); + +export default router; \ No newline at end of file diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 0000000..0b0bc04 --- /dev/null +++ b/src/error.ts @@ -0,0 +1,32 @@ +import { Request, Response, NextFunction } from "express"; + +export function notFound(req: Request, res: Response, next: NextFunction) { + res.status(404); + const error = new Error(`🔍 - Not Found - ${req.originalUrl}`); + next(error); +} + +export function handler(err: Error, req: Request, res: Response, next: NextFunction) { + const statusCode = res.statusCode !== 200 ? res.statusCode : 500; + res.status(statusCode); + + let message; + try { + const jsonMessage = JSON.parse(err.message); + message = jsonMessage; + } catch (e) { + message = err.message; + } + + const responseBody = { + status: statusCode, + name: err.name, + message: message, + stack: process.env.NODE_ENV === "development" ? err.stack : "---" + }; + + //logger.error(responseBody); + res.json(responseBody); + + next(); +} diff --git a/src/models/entry.test.ts b/src/models/entry.test.ts new file mode 100644 index 0000000..bd2c0b4 --- /dev/null +++ b/src/models/entry.test.ts @@ -0,0 +1,47 @@ +import { checkNumber, checkTime } from "./entry"; + + +describe("checkNumber", () => { + it("should throw error if value is not provided", () => { + expect(() => checkNumber(0, 100)("")).toThrow(new Error('is required')); + }); + + it("should throw error if value length is more than 12", () => { + expect(() => checkNumber(0, 100)("1234567890123")).toThrow(new Error('Should have a maximum of 11 digits')); + }); + + it("should throw error if value is not a number", () => { + expect(() => checkNumber(0, 100)("abc")).toThrow(new Error('Value should be between 0 and 100')); + }); + + it("should return true if value is a valid number within range", () => { + expect(checkNumber(0, 100)("50")).toBe(true); + }); +}); + +describe("checkTime", () => { + it("should throw error if value is not a number", () => { + expect(() => checkTime("abc")).toThrow(new Error('Timestamp should be a number')); + }); + + it("should throw error if value is not a valid date", () => { + expect(() => checkTime("99999999999999999999")).toThrow(new Error('Timestamp should represent a valid date')); + }); + + it("should throw error if value is more than 1 day in the past", () => { + const date = new Date(); + date.setDate(date.getDate() - 2); // Set date to 2 days ago + expect(() => checkTime(date.getTime().toString())).toThrow(new Error('Timestamp should represent a date not further from server time than 1 day')); + }); + + it("should throw error if value is more than 1 day in the future", () => { + const date = new Date(); + date.setDate(date.getDate() + 2); // Set date to 2 days in the future + expect(() => checkTime(date.getTime().toString())).toThrow(new Error('Timestamp should represent a date not further from server time than 1 day')); + }); + + it("should return true if value is a valid timestamp within 1 day", () => { + const date = new Date(); + expect(checkTime(date.getTime().toString())).toBe(true); + }); +}); diff --git a/src/models/entry.ts b/src/models/entry.ts new file mode 100644 index 0000000..73b7d29 --- /dev/null +++ b/src/models/entry.ts @@ -0,0 +1,91 @@ +import { Request, Response} from 'express'; +import { checkExact, query } from 'express-validator'; +import { crypt } from '@src/scripts/crypt'; + +export const entry = { + create: (req:Request, res:Response) => { + console.log(req.query); + console.log(res); + }, + validate: [ + query('user').isLength({ min: 2, max: 2 }), + query('lat').custom(checkNumber(-90, 90)), + query('lon').custom(checkNumber(-180, 180)), + query('timestamp').custom(checkTime), + query('hdop').custom(checkNumber(0, 100)), + query('altitude').custom(checkNumber(0, 10000)), + query('speed').custom(checkNumber(0, 300)), + query('heading').custom(checkNumber(0, 360)), + query("key").custom(checkKey), + checkExact() + // INFO: if message or any string gets added remember to escape + ] +} + +export function checkNumber(min:number, max:number) { + return (value:string) => { + if (!value) { + throw new Error('is required'); + } + if (value.length > 12) { + throw new Error('Should have a maximum of 11 digits'); + } + + const number = parseFloat(value); + if (isNaN(number) || number < min || number > max) { + throw new Error(`Value should be between ${min} and ${max}`); + } + return true; + }; +} + +export function checkTime(value:string) { + const timestamp = parseFloat(value); + + // Check if it's a number + if (isNaN(timestamp)) { + throw new Error('Timestamp should be a number'); + } + + // Check if it's a valid date + const date = new Date(timestamp); + if (isNaN(date.getTime())) { + throw new Error('Timestamp should represent a valid date'); + } + + if (process.env.NODE_ENV == "development") { + return true; // dev testing convenience + } + + const now = new Date(); + const difference = now.getTime() - date.getTime(); + const oneDayInMilliseconds = 24 * 60 * 60 * 1000; + if (Math.abs(difference) >= oneDayInMilliseconds) { + throw new Error('Timestamp should represent a date not further from server time than 1 day'); + } + + return true +} + +function checkKey(value:string) { + if (process.env.NODE_ENV != "production" && value == "test") { + return true; // dev testing convenience + } + + if (!value) { + throw new Error('Key required'); + } + + value = decodeURIComponent(value); + + const hash = crypt(value); + + if (process.env.KEYB != hash) { + if (process.env.NODE_ENV == "development") { + console.log(hash); + } + throw new Error('Key does not match'); + } + + return true; +} \ No newline at end of file diff --git a/src/scripts/crypt.ts b/src/scripts/crypt.ts new file mode 100644 index 0000000..b14ef42 --- /dev/null +++ b/src/scripts/crypt.ts @@ -0,0 +1,9 @@ +import * as crypto from 'crypto'; + +export const crypt = function (value:string) { + const key = process.env.KEYA; + if (!key) { + throw new Error('KEYA is not defined in the environment variables'); + } + return crypto.createHmac('sha256', key).update(value).digest("base64"); +}; \ No newline at end of file diff --git a/src/scripts/logger.ts b/src/scripts/logger.ts new file mode 100644 index 0000000..b58f366 --- /dev/null +++ b/src/scripts/logger.ts @@ -0,0 +1,17 @@ +// primitive text logger +import fs from 'fs'; +import path from 'path'; + +const logPath = path.resolve(__dirname, '../httpdocs', 'log.txt'); +const date = new Date().toLocaleString('de-DE', { hour12: false }); + +export default { + log: (message:string|JSON) => { + fs.appendFileSync(logPath, `${date} \t|\t ${message} \n`); + console.log(message); + }, + error: (message:string|JSON|Response.Error) => { + fs.appendFileSync(logPath, `${date} \t|\t ERROR: ${message} \n`); + console.error(message); + }, +} \ No newline at end of file diff --git a/src/scripts/time.ts b/src/scripts/time.ts new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json index 19cde21..613bc08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,13 @@ "module": "CommonJS", "moduleResolution": "node", "target": "ES6", - "sourceMap": true - } + "sourceMap": true, + "baseUrl": "./src", + "paths": { + "@src/*": ["./*"], + } + }, + "files": [ + "types.d.ts" + ] } \ No newline at end of file diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..b3d3397 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,98 @@ + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +type NumericRange = + ARR['length'] extends END ? ACC | START | END : + NumericRange; + +namespace Response { + interface Message { + message: string; + data?: string|JSON; + } + + interface Error extends Response.Message { + stack?: string, + name?: string, + status?: number + } +} +namespace Models { + interface IEntry { + /** + * height above ground in meters, as received by gps + */ + altitude: number, + + /** + * Direction in degrees between two coordinate pairs: 0°-360° + */ + angle: NumericRange<0, 360>, + + /** + * object containing horizontal vertical and total distance, in meters + */ + distance: { + horizontal: number, + vertical: number, + total: number + }, + + /** + * object containing horizontal vertical and total speed, in km/h + */ + speeed: { + horizontal: number, + vertical: number, + total: number + }, + + /** + * index, position of the entry point in the chain + */ + index: number, + + /** + * Heading or Bearing as recieved from gps + */ + heading: NumericRange<0, 360>, + + /** + * lat + */ + lat: number, + + + /** + * lon + */ + lon: number, + + /** + * hdop: accuracy as recieved by gps + */ + hdop: number, + + /** + * ignore: defined by hdop and time difference; determines whether the view shall display this entry + */ + ignore: boolean, + + + /** + * time object containing UNIX timestamps with milliseconds, gps creation time (as recieved via gps), server time (when the server recieved and computed it), differce to last entry (time between waypoints), upload time differnce + */ + time: { + created: number, + recieved: number, + uploadDuration: number, + diff: number + createdString: string + }, + + /** + * user as recieved + */ + user: string + } +} From 781d61d7502246353fb1d5eef8983a97e973fabf Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 25 Jan 2024 21:06:17 +0100 Subject: [PATCH 011/206] [Task] #3 improve error handling, logger and added chalk to colorize console output. Had to use chalk version 4 because of typescript converting to require, and chalk5 do want import syntax. --- package-lock.json | 53 ++++++++++++++++++++++++++++++++++++++++++- package.json | 5 +++- src/error.ts | 13 ++++++++++- src/scripts/logger.ts | 18 ++++++++++----- 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 808a918..e2298f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,14 @@ "express": "^4.18.2", "express-validator": "^7.0.1", "hpp": "^0.2.3", - "module-alias": "^2.2.3" + "module-alias": "^2.2.3", + "node-fetch": "^2.7.0" }, "devDependencies": { "@jest/globals": "^29.7.0", "@tsconfig/node20": "^20.1.2", "@types/bcrypt": "^5.0.2", + "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", @@ -24,6 +26,7 @@ "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", + "chalk": "^4.1.2", "dotenv": "^16.3.1", "eslint": "^8.56.0", "eslint-plugin-jest": "^27.6.3", @@ -1519,6 +1522,16 @@ "@types/node": "*" } }, + "node_modules/@types/dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw==", + "deprecated": "This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "dotenv": "*" + } + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -4989,6 +5002,25 @@ "node": ">= 0.6" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6028,6 +6060,11 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -6327,6 +6364,20 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 43db4ea..6aa27dd 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@jest/globals": "^29.7.0", "@tsconfig/node20": "^20.1.2", "@types/bcrypt": "^5.0.2", + "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", @@ -25,6 +26,7 @@ "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", + "chalk": "^4.1.2", "dotenv": "^16.3.1", "eslint": "^8.56.0", "eslint-plugin-jest": "^27.6.3", @@ -38,7 +40,8 @@ "express": "^4.18.2", "express-validator": "^7.0.1", "hpp": "^0.2.3", - "module-alias": "^2.2.3" + "module-alias": "^2.2.3", + "node-fetch": "^2.7.0" }, "_moduleAliases": { "@src": "dist" diff --git a/src/error.ts b/src/error.ts index 0b0bc04..4f11a2f 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,4 +1,14 @@ import { Request, Response, NextFunction } from "express"; +import logger from '@src/scripts/logger'; + +export function create(res:Response, status:number = 500, message:string, next:NextFunction) { + /** + * takes httpStatusCode and Message and forwards to error Handling + */ + const error = new Error(message); + res.status(status); + return next(error) +} export function notFound(req: Request, res: Response, next: NextFunction) { res.status(404); @@ -7,6 +17,7 @@ export function notFound(req: Request, res: Response, next: NextFunction) { } export function handler(err: Error, req: Request, res: Response, next: NextFunction) { + const statusCode = res.statusCode !== 200 ? res.statusCode : 500; res.status(statusCode); @@ -25,7 +36,7 @@ export function handler(err: Error, req: Request, res: Response stack: process.env.NODE_ENV === "development" ? err.stack : "---" }; - //logger.error(responseBody); + logger.error(responseBody); res.json(responseBody); next(); diff --git a/src/scripts/logger.ts b/src/scripts/logger.ts index b58f366..06479f7 100644 --- a/src/scripts/logger.ts +++ b/src/scripts/logger.ts @@ -1,17 +1,23 @@ // primitive text logger -import fs from 'fs'; -import path from 'path'; +import fs from 'fs'; // typescript will compile to require +import path from 'path'; // typescript will compile to require +import chalk from "chalk"; // keep import syntax after compile const logPath = path.resolve(__dirname, '../httpdocs', 'log.txt'); const date = new Date().toLocaleString('de-DE', { hour12: false }); export default { - log: (message:string|JSON) => { + log: (message:string|JSON, showDateInConsole:boolean=false, showLogInTest=false) => { fs.appendFileSync(logPath, `${date} \t|\t ${message} \n`); - console.log(message); + if (showDateInConsole) { + message = `${chalk.gray(date + ":")} ${message}`; + } + if (process.env.NODE_ENV == "development" || showLogInTest && process.env.NODE_ENV == "test") { + console.log(message); + } }, error: (message:string|JSON|Response.Error) => { fs.appendFileSync(logPath, `${date} \t|\t ERROR: ${message} \n`); console.error(message); - }, -} \ No newline at end of file + } +} From 6d8463872c8489fc06f4b2c28711d38377a70cd1 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 25 Jan 2024 21:07:18 +0100 Subject: [PATCH 012/206] [Change] #3 nodemon to clear console when in dev mode --- nodemon.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nodemon.json b/nodemon.json index 92b25d2..ad7eb01 100644 --- a/nodemon.json +++ b/nodemon.json @@ -2,5 +2,8 @@ "watch": ["src"], "ext": ".ts", "ignore": [], - "exec": "tsc && node dist/app.js" + "exec": "tsc && node dist/app.js", + "events": { + "start": "clear" + } } \ No newline at end of file From 63085ed80aa33d6dd007492a5a24b025812f7ff8 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 25 Jan 2024 21:08:04 +0100 Subject: [PATCH 013/206] [!Task] #32 webhook creates folder and file based on date --- .gitignore | 2 ++ src/app.ts | 3 +-- src/controller/write.ts | 20 ++++++++-------- src/models/entry.ts | 12 ++++++---- src/scripts/file.ts | 51 +++++++++++++++++++++++++++++++++++++++++ types.d.ts | 23 ++++++++++++------- 6 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 src/scripts/file.ts diff --git a/.gitignore b/.gitignore index c6bba59..c53815f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +data/ + # Logs logs *.log diff --git a/src/app.ts b/src/app.ts index 382c255..8b3fd8d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,7 +8,6 @@ import writeRouter from '@src/controller/write'; import path from 'path'; import logger from '@src/scripts/logger'; - // configurations config(); const app = express(); @@ -30,5 +29,5 @@ app.use(error.handler); // init server app.listen(80, () => { - logger.log(`Server running //localhost:80`); + logger.log(`Server running //localhost:80`, true); }); \ No newline at end of file diff --git a/src/controller/write.ts b/src/controller/write.ts index b62fd57..4bf9a09 100644 --- a/src/controller/write.ts +++ b/src/controller/write.ts @@ -1,17 +1,19 @@ import express, { Request, Response, NextFunction } from 'express'; import { entry } from '@src/models/entry'; import { validationResult } from 'express-validator'; +import { create as createError } from '@src/error'; + // example call: /write?user=xx&lat=00.000&lon=00.000×tamp=1704063600000&hdop=0.0&altitude=0.000&speed=0.000&heading=000.0 function errorChecking (req:Request, res:Response, next:NextFunction) { const errors = validationResult(req); if (!errors.isEmpty()) { const errorAsJson = { errors: errors.array()}; - const errorAsString = new Error(JSON.stringify(errorAsJson)); + const errorAsString = JSON.stringify(errorAsJson); const hasKeyErrors = errors.array().some(error => error.msg.includes("Key")); - res.status(hasKeyErrors ? 403 : 422); // send forbidden or unprocessable content - return next(errorAsString); + // send forbidden or unprocessable content + return createError(res, hasKeyErrors ? 403 : 422, errorAsString, next) } if (req.method == "HEAD") { @@ -19,12 +21,12 @@ function errorChecking (req:Request, res:Response, next:NextFunction) { return; } - // Regular Save logic from here - - //entry.create(req, res); - //const test = process.env.TEST; - // res.send(req.query); - + // Regular Save logic from here + entry.create(req, res, next); + + + + res.send(req.query); } diff --git a/src/models/entry.ts b/src/models/entry.ts index 73b7d29..5f9088f 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -1,11 +1,15 @@ -import { Request, Response} from 'express'; +import { NextFunction, Request, Response} from 'express'; import { checkExact, query } from 'express-validator'; import { crypt } from '@src/scripts/crypt'; +import * as file from '@src/scripts/file'; + export const entry = { - create: (req:Request, res:Response) => { - console.log(req.query); - console.log(res); + create: async (req:Request, res:Response, next:NextFunction) => { + const fileObj:File.Obj= file.createFile(res, next); + fileObj.content = await file.readAsJson(res, fileObj.path, next); + + console.log(fileObj.content); }, validate: [ query('user').isLength({ min: 2, max: 2 }), diff --git a/src/scripts/file.ts b/src/scripts/file.ts new file mode 100644 index 0000000..a7ba192 --- /dev/null +++ b/src/scripts/file.ts @@ -0,0 +1,51 @@ +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; +import { create as createError } from '@src/error'; +import { NextFunction, Response } from 'express'; +import logger from '@src/scripts/logger'; + +export const createFile = (res: Response, next: NextFunction): File.Obj => { + const date = new Date(); + const formattedDate = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; + const dirPath = path.resolve(__dirname, '../data'); + const filePath = path.resolve(dirPath, `data-${formattedDate}.json`); + + if (!fs.existsSync(dirPath)){ + fs.mkdirSync(dirPath, { recursive: true }); + logger.log("data folder did not exist, but created now"); + } + + let fileExisted = true; + if (!fs.existsSync(filePath)) { // check if file exist + fileExisted = false; + try { + // fs.appendFileSync(filePath, 'test'); + fs.writeFileSync(filePath, ''); + } catch (err) { + createError(res, 500, "File cannot be written to", next); + } + } + + return { path: filePath, content: fileExisted ? undefined : '' }; // if the file did not exist before, the content is emptyString +}; + + + + +const readFileAsync = promisify(fs.readFile); + +export async function readAsJson(res: Response, filePath: string, next: NextFunction): Promise { + const data = await readFileAsync(filePath, 'utf-8'); + console.log(data); + + if (data === '') { + return ''; + } + try { + return JSON.parse(data); + } catch (err) { + createError(res, 500, "File contains wrong content", next); + return undefined; + } +} diff --git a/types.d.ts b/types.d.ts index b3d3397..9703981 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,14 +1,14 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -type NumericRange = - ARR['length'] extends END ? ACC | START | END : - NumericRange; +type NumericRange = + ARR['length'] extends END ? ACC | START | END : + NumericRange; namespace Response { interface Message { message: string; - data?: string|JSON; + data?: string | JSON; } interface Error extends Response.Message { @@ -17,6 +17,13 @@ namespace Response { status?: number } } +namespace File { + interface Obj { + path: string + content?: JSON | '' + } +} + namespace Models { interface IEntry { /** @@ -33,9 +40,9 @@ namespace Models { * object containing horizontal vertical and total distance, in meters */ distance: { - horizontal: number, - vertical: number, - total: number + horizontal: number, + vertical: number, + total: number }, /** @@ -61,7 +68,7 @@ namespace Models { * lat */ lat: number, - + /** * lon From 8873654b0394929394400f62b4eb16514997ee98 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 26 Jan 2024 16:46:25 +0100 Subject: [PATCH 014/206] [Change] #35 relocated tests and refactor write, also added file check --- src/controller/write.test.ts | 100 ---------------------------- src/controller/write.ts | 2 - src/scripts/file.ts | 1 - src/{ => tests}/app.test.ts | 2 +- src/{models => tests}/entry.test.ts | 2 +- src/tests/write.test.ts | 87 ++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 105 deletions(-) delete mode 100644 src/controller/write.test.ts rename src/{ => tests}/app.test.ts (81%) rename src/{models => tests}/entry.test.ts (97%) create mode 100644 src/tests/write.test.ts diff --git a/src/controller/write.test.ts b/src/controller/write.test.ts deleted file mode 100644 index f447d22..0000000 --- a/src/controller/write.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import axios, { AxiosError } from 'axios'; - -describe('HEAD /write', () => { - it('with all parameters correctly set it should succeed', async () => { - const timestamp = new Date().getTime(); - const response = await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - expect(response.status).toBe(200); - }); - - it('without key it sends 403', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(403); - } - }); - - it('with user length not equal to 2 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=x&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - - it('with lat not between -90 and 90 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=91.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - it('with lon not between -180 and 180 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=181.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - it('with timestamp to old sends 422', async () => { - try { - const timestamp = new Date().getTime() - 24 * 60 * 60 * 1000 * 2; // two days ago - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }) - - it('with hdop not between 0 and 100 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - it('with altitude not between 0 and 10000 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - it('with speed not between 0 and 300 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - it('with heading not between 0 and 360 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); -}); diff --git a/src/controller/write.ts b/src/controller/write.ts index 4bf9a09..4b61d30 100644 --- a/src/controller/write.ts +++ b/src/controller/write.ts @@ -24,8 +24,6 @@ function errorChecking (req:Request, res:Response, next:NextFunction) { // Regular Save logic from here entry.create(req, res, next); - - res.send(req.query); } diff --git a/src/scripts/file.ts b/src/scripts/file.ts index a7ba192..75040a0 100644 --- a/src/scripts/file.ts +++ b/src/scripts/file.ts @@ -20,7 +20,6 @@ export const createFile = (res: Response, next: NextFunction): File.Obj => { if (!fs.existsSync(filePath)) { // check if file exist fileExisted = false; try { - // fs.appendFileSync(filePath, 'test'); fs.writeFileSync(filePath, ''); } catch (err) { createError(res, 500, "File cannot be written to", next); diff --git a/src/app.test.ts b/src/tests/app.test.ts similarity index 81% rename from src/app.test.ts rename to src/tests/app.test.ts index 0682f2d..453caf4 100644 --- a/src/app.test.ts +++ b/src/tests/app.test.ts @@ -4,7 +4,7 @@ describe('Server Status', () => { it('The server is running', async () => { let serverStatus; try { - const response = await axios.get('http://localhost'); + const response = await axios.get('http://localhost:80'); serverStatus = response.status; } catch (error) { console.error(error); diff --git a/src/models/entry.test.ts b/src/tests/entry.test.ts similarity index 97% rename from src/models/entry.test.ts rename to src/tests/entry.test.ts index bd2c0b4..48058e7 100644 --- a/src/models/entry.test.ts +++ b/src/tests/entry.test.ts @@ -1,4 +1,4 @@ -import { checkNumber, checkTime } from "./entry"; +import { checkNumber, checkTime } from "../models/entry"; describe("checkNumber", () => { diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts new file mode 100644 index 0000000..2a61ef8 --- /dev/null +++ b/src/tests/write.test.ts @@ -0,0 +1,87 @@ +import axios, { AxiosError } from 'axios'; +import fs from "fs"; +import path from "path"; + +async function callServer(timestamp = new Date().getTime(), query: string, expectStatus: number = 200, method:string = "HEAD") { + const url = new URL("http://localhost:80/write?"); + url.search = "?" + query; + const params = new URLSearchParams(url.search); + params.set("timestamp", timestamp.toString()); + url.search = params.toString(); + + let response; + if (expectStatus == 200) { + if (method == "GET") { + response = await axios.get(url.toString()); + } else { + response = await axios.head(url.toString()); + } + expect(response.status).toBe(expectStatus); + } else { + try { + await axios.head(url.toString()); + } catch (error) { + const axiosError = error as AxiosError; + expect(axiosError.response!.status).toBe(expectStatus); + } + } +} + +describe('HEAD /write', () => { + it('with all parameters correctly set it should succeed', async () => { + callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200); + }); + + it('without key it sends 403', async () => { + callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0", 403); + }); + + it('with user length not equal to 2 it sends 422', async () => { + callServer(undefined, "user=x&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }); + + it('with lat not between -90 and 90 it sends 422', async () => { + callServer(undefined, "user=xx&lat=91.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }); + + it('with lon not between -180 and 180 it sends 422', async () => { + callServer(undefined, "user=xx&lat=45.000&lon=181.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }); + + it('with timestamp to old sends 422', async () => { + const timestamp = new Date().getTime() - 24 * 60 * 60 * 1000 * 2; // two days ago + callServer(timestamp, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }) + + it('with hdop not between 0 and 100 it sends 422', async () => { + callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }); + + it('with altitude not between 0 and 10000 it sends 422', async () => { + callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test", 422); + }); + + it('with speed not between 0 and 300 it sends 422', async () => { + callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test", 422); + }); + + it('with heading not between 0 and 360 it sends 422', async () => { + callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test", 422); + }); +}); + + +describe("GET /write", () => { + it('there should a file of the current date', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + + const date = new Date(); + const formattedDate = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; + const dirPath = path.resolve(__dirname, '../../dist/data/'); + const filePath = path.resolve(dirPath, `data-${formattedDate}.json`); + console.log(filePath); + fs.access(filePath, fs.constants.F_OK, (err) => { + expect(err).toBeFalsy(); + }); + }); +}); \ No newline at end of file From 91fba3bf1722ff232fa31d3787151ae129b6a4fe Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 26 Jan 2024 17:14:54 +0100 Subject: [PATCH 015/206] [Task] #18, installed helmet, configured self as CSP origin --- package-lock.json | 9 +++++++++ package.json | 1 + src/app.ts | 14 ++++++++++++++ src/scripts/file.ts | 28 +++++++++++++--------------- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2298f3..6632d1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "express": "^4.18.2", "express-validator": "^7.0.1", + "helmet": "^7.1.0", "hpp": "^0.2.3", "module-alias": "^2.2.3", "node-fetch": "^2.7.0" @@ -3769,6 +3770,14 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz", + "integrity": "sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/hpp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", diff --git a/package.json b/package.json index 6aa27dd..c5fd153 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "dependencies": { "express": "^4.18.2", "express-validator": "^7.0.1", + "helmet": "^7.1.0", "hpp": "^0.2.3", "module-alias": "^2.2.3", "node-fetch": "^2.7.0" diff --git a/src/app.ts b/src/app.ts index 8b3fd8d..fd0f1ea 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,7 @@ require('module-alias/register'); import { config } from 'dotenv'; import express from 'express'; +import helmet from 'helmet'; import hpp from 'hpp'; import cache from './cache'; import * as error from "./error"; @@ -11,6 +12,19 @@ import logger from '@src/scripts/logger'; // configurations config(); const app = express(); +app.use( + helmet({ + contentSecurityPolicy: { + directives: { + "default-src": "self", + "script-src": "self", + "img-src": "*", + "media-src": "self" + }, + }, + }), +); + app.use(hpp()); app.use(cache); diff --git a/src/scripts/file.ts b/src/scripts/file.ts index 75040a0..9660fcf 100644 --- a/src/scripts/file.ts +++ b/src/scripts/file.ts @@ -11,19 +11,19 @@ export const createFile = (res: Response, next: NextFunction): File.Obj => { const dirPath = path.resolve(__dirname, '../data'); const filePath = path.resolve(dirPath, `data-${formattedDate}.json`); - if (!fs.existsSync(dirPath)){ - fs.mkdirSync(dirPath, { recursive: true }); - logger.log("data folder did not exist, but created now"); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + logger.log("data folder did not exist, but created now"); } let fileExisted = true; if (!fs.existsSync(filePath)) { // check if file exist - fileExisted = false; - try { - fs.writeFileSync(filePath, ''); - } catch (err) { - createError(res, 500, "File cannot be written to", next); - } + fileExisted = false; + try { + fs.writeFileSync(filePath, ''); + } catch (err) { + createError(res, 500, "File cannot be written to", next); + } } return { path: filePath, content: fileExisted ? undefined : '' }; // if the file did not exist before, the content is emptyString @@ -36,14 +36,12 @@ const readFileAsync = promisify(fs.readFile); export async function readAsJson(res: Response, filePath: string, next: NextFunction): Promise { const data = await readFileAsync(filePath, 'utf-8'); - console.log(data); - if (data === '') { - return ''; - } - try { + if (data === '') { return ''; } + + try { return JSON.parse(data); - } catch (err) { + } catch (err) { createError(res, 500, "File contains wrong content", next); return undefined; } From a22f970e2c739337da56b875e942dc07e0f3df8a Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 29 Jan 2024 08:44:55 +0100 Subject: [PATCH 016/206] [Fix] moved chalk out of dev dependency --- package-lock.json | 51 +++---------------------------------------- package.json | 5 ++--- src/scripts/logger.ts | 2 +- 3 files changed, 6 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6632d1b..4c0b355 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,12 @@ "name": "lorex", "version": "0.0.1", "dependencies": { + "chalk": "^4.1.2", "express": "^4.18.2", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", - "module-alias": "^2.2.3", - "node-fetch": "^2.7.0" + "module-alias": "^2.2.3" }, "devDependencies": { "@jest/globals": "^29.7.0", @@ -27,7 +27,6 @@ "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", - "chalk": "^4.1.2", "dotenv": "^16.3.1", "eslint": "^8.56.0", "eslint-plugin-jest": "^27.6.3", @@ -2112,7 +2111,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2452,7 +2450,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2468,7 +2465,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2477,7 +2473,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -2576,7 +2571,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2587,8 +2581,7 @@ "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==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -5011,25 +5004,6 @@ "node": ">= 0.6" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6069,11 +6043,6 @@ "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -6373,20 +6342,6 @@ "makeerror": "1.0.12" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index c5fd153..7e450e9 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", - "chalk": "^4.1.2", "dotenv": "^16.3.1", "eslint": "^8.56.0", "eslint-plugin-jest": "^27.6.3", @@ -37,12 +36,12 @@ "typescript": "^5.3.3" }, "dependencies": { + "chalk": "^4.1.2", "express": "^4.18.2", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", - "module-alias": "^2.2.3", - "node-fetch": "^2.7.0" + "module-alias": "^2.2.3" }, "_moduleAliases": { "@src": "dist" diff --git a/src/scripts/logger.ts b/src/scripts/logger.ts index 06479f7..9cbf34b 100644 --- a/src/scripts/logger.ts +++ b/src/scripts/logger.ts @@ -10,7 +10,7 @@ export default { log: (message:string|JSON, showDateInConsole:boolean=false, showLogInTest=false) => { fs.appendFileSync(logPath, `${date} \t|\t ${message} \n`); if (showDateInConsole) { - message = `${chalk.gray(date + ":")} ${message}`; + message = `${chalk.dim(date + ":")} ${message}`; } if (process.env.NODE_ENV == "development" || showLogInTest && process.env.NODE_ENV == "test") { console.log(message); From c779de643a605cf4c7f37a9d83b200d8556053f8 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 29 Jan 2024 09:06:06 +0100 Subject: [PATCH 017/206] [Task] #32 error logging and text output improvement, log string instead of "object" --- src/scripts/logger.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scripts/logger.ts b/src/scripts/logger.ts index 9cbf34b..0beeaea 100644 --- a/src/scripts/logger.ts +++ b/src/scripts/logger.ts @@ -8,6 +8,7 @@ const date = new Date().toLocaleString('de-DE', { hour12: false }); export default { log: (message:string|JSON, showDateInConsole:boolean=false, showLogInTest=false) => { + message = JSON.stringify(message); fs.appendFileSync(logPath, `${date} \t|\t ${message} \n`); if (showDateInConsole) { message = `${chalk.dim(date + ":")} ${message}`; From 8a5137d0578259a4793a1bb91af452a36b394edb Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 29 Jan 2024 09:09:19 +0100 Subject: [PATCH 018/206] [Task] #18 CSP Update to allow localhost for testing --- src/app.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app.ts b/src/app.ts index fd0f1ea..d65799a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,10 +16,8 @@ app.use( helmet({ contentSecurityPolicy: { directives: { - "default-src": "self", - "script-src": "self", - "img-src": "*", - "media-src": "self" + "default-src": "'self'", + "img-src": "*" }, }, }), @@ -43,5 +41,5 @@ app.use(error.handler); // init server app.listen(80, () => { - logger.log(`Server running //localhost:80`, true); + logger.log(`Server running //localhost:80, ENV: ${process.env.NODE_ENV}`, true); }); \ No newline at end of file From 3e8dab11a20a5bf1d5c6dfa60ee01d6c87d7520c Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 29 Jan 2024 18:40:15 +0100 Subject: [PATCH 019/206] [Fix] #3 debugging setup improvments --- .vscode/launch.json | 32 +++++++++++++------------------- .vscode/tasks.json | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json index c56ec22..8425a2b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,21 +1,15 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Debug server.ts", - "skipFiles": [ - "/**" - ], - "program": "${workspaceFolder}\\src\\app.ts", - "preLaunchTask": "tsc: build - tsconfig.json", - "outFiles": [ - "${workspaceFolder}/dist/**/*.js" - ] - } - ] + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug TypeScript", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}\\src\\app.ts", + "preLaunchTask": "build" + } + ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..864577f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "npx tsc -p tsconfig.json", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$tsc" + } + ] +} From ff7c28736a560c16cc45a24a663052879b7a12ef Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 29 Jan 2024 18:41:11 +0100 Subject: [PATCH 020/206] [FIX] #10 Error Handling --- src/controller/write.ts | 10 +++++++--- src/error.ts | 5 ++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/controller/write.ts b/src/controller/write.ts index 4b61d30..2251b11 100644 --- a/src/controller/write.ts +++ b/src/controller/write.ts @@ -5,7 +5,7 @@ import { create as createError } from '@src/error'; // example call: /write?user=xx&lat=00.000&lon=00.000×tamp=1704063600000&hdop=0.0&altitude=0.000&speed=0.000&heading=000.0 -function errorChecking (req:Request, res:Response, next:NextFunction) { +async function errorChecking (req:Request, res:Response, next:NextFunction) { const errors = validationResult(req); if (!errors.isEmpty()) { const errorAsJson = { errors: errors.array()}; @@ -22,9 +22,13 @@ function errorChecking (req:Request, res:Response, next:NextFunction) { } // Regular Save logic from here - entry.create(req, res, next); + await entry.create(req, res, next); - res.send(req.query); + if (!res.locals.error) { + res.send(req.query); + } + + next(); } diff --git a/src/error.ts b/src/error.ts index 4f11a2f..7b8d624 100644 --- a/src/error.ts +++ b/src/error.ts @@ -7,7 +7,8 @@ export function create(res:Response, status:number = 500, message:string, next:N */ const error = new Error(message); res.status(status); - return next(error) + res.locals.error = true; // to let other middleware know that an error was called + next(error) } export function notFound(req: Request, res: Response, next: NextFunction) { @@ -17,7 +18,6 @@ export function notFound(req: Request, res: Response, next: NextFunction) { } export function handler(err: Error, req: Request, res: Response, next: NextFunction) { - const statusCode = res.statusCode !== 200 ? res.statusCode : 500; res.status(statusCode); @@ -38,6 +38,5 @@ export function handler(err: Error, req: Request, res: Response logger.error(responseBody); res.json(responseBody); - next(); } From 10391c5ddf571f2db74618cbcced60da0a9cfe89 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 29 Jan 2024 18:41:48 +0100 Subject: [PATCH 021/206] [Task] #10 writing basic non calculated data to file --- src/models/entry.ts | 99 ++++++++++++++++++++++++++++++--------------- src/scripts/file.ts | 34 +++++++++++----- types.d.ts | 19 ++++----- 3 files changed, 100 insertions(+), 52 deletions(-) diff --git a/src/models/entry.ts b/src/models/entry.ts index 5f9088f..3197738 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -1,51 +1,84 @@ -import { NextFunction, Request, Response} from 'express'; +import { NextFunction, Request, Response } from 'express'; import { checkExact, query } from 'express-validator'; import { crypt } from '@src/scripts/crypt'; +import { create as createError } from '@src/error'; import * as file from '@src/scripts/file'; export const entry = { - create: async (req:Request, res:Response, next:NextFunction) => { - const fileObj:File.Obj= file.createFile(res, next); + create: async (req: Request, res: Response, next: NextFunction) => { + const fileObj: File.Obj = file.getFile(res, next); fileObj.content = await file.readAsJson(res, fileObj.path, next); - console.log(fileObj.content); - }, - validate: [ - query('user').isLength({ min: 2, max: 2 }), - query('lat').custom(checkNumber(-90, 90)), - query('lon').custom(checkNumber(-180, 180)), - query('timestamp').custom(checkTime), - query('hdop').custom(checkNumber(0, 100)), - query('altitude').custom(checkNumber(0, 10000)), - query('speed').custom(checkNumber(0, 300)), - query('heading').custom(checkNumber(0, 360)), + if (!fileObj.content?.entries) { + return createError(res, 500, "File Content unavailable: " + fileObj.path, next); + } + const entries = fileObj.content.entries; + const entry = {} as Models.IEntry; + + entry.altitude = Number(req.query.altitude); + + entry.hdop = Number(req.query.hdop); + entry.heading = Number(req.query.heading); + entry.index = entries.length; + entry.lat = Number(req.query.lat); + entry.lon = Number(req.query.lon); + entry.user = req.query.user as string; + // entry.time = getTime(); + // entry.speed = getSpeed() + if (entries.length) { // so there is a previous entry + // checkIgnore() + // newEntry.angle = getAngle(); + // newEntry.distance = getDistance() + } + + + + + + + entries.push(entry); + + + file.write(res, fileObj, next); + + + }, + validate: [ + query('user').isLength({ min: 2, max: 2 }), + query('lat').custom(checkNumber(-90, 90)), + query('lon').custom(checkNumber(-180, 180)), + query('timestamp').custom(checkTime), + query('hdop').custom(checkNumber(0, 100)), + query('altitude').custom(checkNumber(0, 10000)), + query('speed').custom(checkNumber(0, 300)), + query('heading').custom(checkNumber(0, 360, "integer")), query("key").custom(checkKey), - checkExact() + checkExact() // INFO: if message or any string gets added remember to escape - ] + ] } -export function checkNumber(min:number, max:number) { - return (value:string) => { +export function checkNumber(min: number, max: number, type: string = "float") { + return (value: string) => { if (!value) { throw new Error('is required'); } - if (value.length > 12) { - throw new Error('Should have a maximum of 11 digits'); - } - - const number = parseFloat(value); + if (value.length > 12) { + throw new Error('Should have a maximum of 11 digits'); + } + + const number = type == "float" ? parseFloat(value) : parseInt(value); if (isNaN(number) || number < min || number > max) { throw new Error(`Value should be between ${min} and ${max}`); } return true; - }; + }; } -export function checkTime(value:string) { +export function checkTime(value: string) { const timestamp = parseFloat(value); - + // Check if it's a number if (isNaN(timestamp)) { throw new Error('Timestamp should be a number'); @@ -60,20 +93,20 @@ export function checkTime(value:string) { if (process.env.NODE_ENV == "development") { return true; // dev testing convenience } - + const now = new Date(); const difference = now.getTime() - date.getTime(); const oneDayInMilliseconds = 24 * 60 * 60 * 1000; if (Math.abs(difference) >= oneDayInMilliseconds) { throw new Error('Timestamp should represent a date not further from server time than 1 day'); } - + return true } -function checkKey(value:string) { +function checkKey(value: string) { if (process.env.NODE_ENV != "production" && value == "test") { - return true; // dev testing convenience + return true; // dev testing convenience } if (!value) { @@ -81,13 +114,13 @@ function checkKey(value:string) { } value = decodeURIComponent(value); - + const hash = crypt(value); if (process.env.KEYB != hash) { if (process.env.NODE_ENV == "development") { - console.log(hash); - } + console.log(hash); + } throw new Error('Key does not match'); } diff --git a/src/scripts/file.ts b/src/scripts/file.ts index 9660fcf..9988a67 100644 --- a/src/scripts/file.ts +++ b/src/scripts/file.ts @@ -5,7 +5,7 @@ import { create as createError } from '@src/error'; import { NextFunction, Response } from 'express'; import logger from '@src/scripts/logger'; -export const createFile = (res: Response, next: NextFunction): File.Obj => { +export const getFile = (res: Response, next: NextFunction): File.Obj => { const date = new Date(); const formattedDate = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; const dirPath = path.resolve(__dirname, '../data'); @@ -20,29 +20,43 @@ export const createFile = (res: Response, next: NextFunction): File.Obj => { if (!fs.existsSync(filePath)) { // check if file exist fileExisted = false; try { - fs.writeFileSync(filePath, ''); + fs.writeFileSync(filePath, '{"entries": []}'); + logger.log(`file: ${filePath} did not exist, but created now`); } catch (err) { createError(res, 500, "File cannot be written to", next); } } - return { path: filePath, content: fileExisted ? undefined : '' }; // if the file did not exist before, the content is emptyString + return { path: filePath, content: fileExisted ? undefined : JSON.parse('{"entries": []}') }; // if the file did not exist before, the content is emptyString }; - - const readFileAsync = promisify(fs.readFile); -export async function readAsJson(res: Response, filePath: string, next: NextFunction): Promise { +export async function readAsJson(res: Response, filePath: string, next: NextFunction): Promise { const data = await readFileAsync(filePath, 'utf-8'); - if (data === '') { return ''; } - try { return JSON.parse(data); } catch (err) { - createError(res, 500, "File contains wrong content", next); - return undefined; + createError(res, 500, "File contains wrong content: " + filePath, next); } } + + +export const write = (res:Response, fileObj:File.Obj, next: NextFunction) => { + + if (!fs.existsSync(fileObj.path)) { // check if file exist + createError(res, 500, "Can not write to file that does not exist: " + fileObj.path, next); + } + try { + const content = JSON.stringify(fileObj.content); + fs.writeFileSync(fileObj.path, content); + fileObj.content = JSON.parse(content); + logger.log(`written to file: ${fileObj.path}`); + } catch (err) { + createError(res, 500, `File (${fileObj.path}) cannot be written to`, next); + } + + return fileObj; // if the file did not exist before, the content is emptyString +}; diff --git a/types.d.ts b/types.d.ts index 9703981..8341c1e 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,10 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -type NumericRange = - ARR['length'] extends END ? ACC | START | END : - NumericRange; - namespace Response { interface Message { message: string; @@ -19,12 +15,16 @@ namespace Response { } namespace File { interface Obj { - path: string - content?: JSON | '' + path: string, + content?: Models.IEntries; } } namespace Models { + interface IEntries { + entries: Models.IEntry[] + } + interface IEntry { /** * height above ground in meters, as received by gps @@ -34,7 +34,7 @@ namespace Models { /** * Direction in degrees between two coordinate pairs: 0°-360° */ - angle: NumericRange<0, 360>, + angle: number, /** * object containing horizontal vertical and total distance, in meters @@ -48,7 +48,8 @@ namespace Models { /** * object containing horizontal vertical and total speed, in km/h */ - speeed: { + speed: { + gps: number; horizontal: number, vertical: number, total: number @@ -62,7 +63,7 @@ namespace Models { /** * Heading or Bearing as recieved from gps */ - heading: NumericRange<0, 360>, + heading: number, /** * lat From 552dc323ad65aa0a8b234e7ea712a9a2f354b139 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 30 Jan 2024 12:04:04 +0100 Subject: [PATCH 022/206] [Fix] #10 avoid Header Modification after sending the request --- src/controller/write.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/controller/write.ts b/src/controller/write.ts index 2251b11..e4c6ef0 100644 --- a/src/controller/write.ts +++ b/src/controller/write.ts @@ -24,11 +24,14 @@ async function errorChecking (req:Request, res:Response, next:NextFunction) { // Regular Save logic from here await entry.create(req, res, next); - if (!res.locals.error) { + if (!res.locals.error) { res.send(req.query); - } - - next(); + } else { + /* at this point error handling already happend, + * or the request has already been send + * therefor there is no need for it again (only middleware to follow at this point) */ + next(); + } } From 7bfdceda163496705c6612da2c8cfce0d9482d5b Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 30 Jan 2024 13:34:49 +0100 Subject: [PATCH 023/206] [Task] #10 JSON Data pretty output --- src/scripts/file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/file.ts b/src/scripts/file.ts index 9988a67..5f0cbe5 100644 --- a/src/scripts/file.ts +++ b/src/scripts/file.ts @@ -50,7 +50,7 @@ export const write = (res:Response, fileObj:File.Obj, next: NextFunction) => { createError(res, 500, "Can not write to file that does not exist: " + fileObj.path, next); } try { - const content = JSON.stringify(fileObj.content); + const content = JSON.stringify(fileObj.content, undefined, 2); fs.writeFileSync(fileObj.path, content); fileObj.content = JSON.parse(content); logger.log(`written to file: ${fileObj.path}`); From 0bee5737f0d72660b66a085157ed0959429a64d1 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 30 Jan 2024 13:51:49 +0100 Subject: [PATCH 024/206] [Task] #32 update types to reflect subobjects of entry --- types.d.ts | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/types.d.ts b/types.d.ts index 8341c1e..c829a45 100644 --- a/types.d.ts +++ b/types.d.ts @@ -39,21 +39,12 @@ namespace Models { /** * object containing horizontal vertical and total distance, in meters */ - distance: { - horizontal: number, - vertical: number, - total: number - }, + distance: Models.IDistance, /** * object containing horizontal vertical and total speed, in km/h */ - speed: { - gps: number; - horizontal: number, - vertical: number, - total: number - }, + speed: Models.ISpeed, /** * index, position of the entry point in the chain @@ -90,17 +81,31 @@ namespace Models { /** * time object containing UNIX timestamps with milliseconds, gps creation time (as recieved via gps), server time (when the server recieved and computed it), differce to last entry (time between waypoints), upload time differnce */ - time: { - created: number, - recieved: number, - uploadDuration: number, - diff: number - createdString: string - }, + time: Models.time, /** * user as recieved */ user: string } + + interface ITime { + created: number, + recieved: number, + uploadDuration: number, + diff: number + createdString: string + } + + interface ISpeed { + gps: number; + horizontal: number, + vertical: number, + total: number + } + interface IDistance { + horizontal: number, + vertical: number, + total: number + } } From 9e801fe6d67f24a2cf79901eabebc406fae52482 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 30 Jan 2024 13:52:14 +0100 Subject: [PATCH 025/206] [Task] #10 write time --- src/models/entry.ts | 15 ++++++--------- src/scripts/time.ts | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/models/entry.ts b/src/models/entry.ts index 3197738..285a061 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -3,6 +3,8 @@ import { checkExact, query } from 'express-validator'; import { crypt } from '@src/scripts/crypt'; import { create as createError } from '@src/error'; import * as file from '@src/scripts/file'; +import { getTime } from '@src/scripts/time'; +import { getSpeed } from '@src/scripts/speed'; export const entry = { @@ -17,32 +19,27 @@ export const entry = { const entry = {} as Models.IEntry; entry.altitude = Number(req.query.altitude); - entry.hdop = Number(req.query.hdop); entry.heading = Number(req.query.heading); entry.index = entries.length; entry.lat = Number(req.query.lat); entry.lon = Number(req.query.lon); entry.user = req.query.user as string; - // entry.time = getTime(); - // entry.speed = getSpeed() + //entry.speed = getSpeed(Number(req.query.speed)) if (entries.length) { // so there is a previous entry + entry.time = getTime(Number(req.query.timestamp), entries.at(-1)); // checkIgnore() // newEntry.angle = getAngle(); // newEntry.distance = getDistance() + } else { + entry.time = getTime(Number(req.query.timestamp)); } - - - - entries.push(entry); - file.write(res, fileObj, next); - }, validate: [ query('user').isLength({ min: 2, max: 2 }), diff --git a/src/scripts/time.ts b/src/scripts/time.ts index e69de29..6a988ec 100644 --- a/src/scripts/time.ts +++ b/src/scripts/time.ts @@ -0,0 +1,21 @@ +export function getTime(time: number, entry?: Models.IEntry): Models.ITime { + const now = new Date(); + const created = Number(time); + const recieved = now.getTime(); + const uploadDuration = recieved - created; + const createdString = now.toLocaleString("de-DE", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); + const diff = entry ? created - entry.time.created : -1; + + return { + created: created, + recieved: recieved, + uploadDuration: uploadDuration, + diff: diff, + createdString: createdString + } +} \ No newline at end of file From b801af41c2757b6a24cf4766d609732d78fca6e0 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 30 Jan 2024 14:04:51 +0100 Subject: [PATCH 026/206] [Task] #32 added logging for time edgecases --- src/scripts/time.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/scripts/time.ts b/src/scripts/time.ts index 6a988ec..453857f 100644 --- a/src/scripts/time.ts +++ b/src/scripts/time.ts @@ -1,3 +1,5 @@ +import logger from '@src/scripts/logger'; + export function getTime(time: number, entry?: Models.IEntry): Models.ITime { const now = new Date(); const created = Number(time); @@ -11,6 +13,13 @@ export function getTime(time: number, entry?: Models.IEntry): Models.ITime { }); const diff = entry ? created - entry.time.created : -1; + if (uploadDuration < 0) { + logger.error(`upload Duration is negative: ${createdString}, index: ${entry ? entry.index + 1 : 0}`); + } + if (entry && entry.time.created > created) { // maybe this could happend due to the async nature, but due to uncertainty logging is enabled + logger.error(`previous timestamp is more recent: ${createdString}, index: ${entry?.index + 1}`); + } + return { created: created, recieved: recieved, From b7360b27c583284679ee31a604b69d9e39ee7ce5 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 30 Jan 2024 23:26:47 +0100 Subject: [PATCH 027/206] [Task] #10 output seconds --- src/scripts/time.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/scripts/time.ts b/src/scripts/time.ts index 453857f..f31bb39 100644 --- a/src/scripts/time.ts +++ b/src/scripts/time.ts @@ -10,7 +10,9 @@ export function getTime(time: number, entry?: Models.IEntry): Models.ITime { year: "numeric", month: "long", day: "numeric", - }); + hour12: false, + minute: '2-digit', + }) + ":" + now.getSeconds(); const diff = entry ? created - entry.time.created : -1; if (uploadDuration < 0) { From 4cd1c07bada66544c4e13b1208dd2ec1daa4c9ae Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 30 Jan 2024 23:31:31 +0100 Subject: [PATCH 028/206] [Task] #10 calculate distance based on lat and lon --- src/models/entry.ts | 13 ++++++++----- src/scripts/distance.ts | 30 ++++++++++++++++++++++++++++++ src/scripts/time.ts | 1 + 3 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 src/scripts/distance.ts diff --git a/src/models/entry.ts b/src/models/entry.ts index 285a061..2ebe35c 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -4,7 +4,8 @@ import { crypt } from '@src/scripts/crypt'; import { create as createError } from '@src/error'; import * as file from '@src/scripts/file'; import { getTime } from '@src/scripts/time'; -import { getSpeed } from '@src/scripts/speed'; +//import { getSpeed } from '@src/scripts/speed'; +import { getDistance } from '@src/scripts/distance'; export const entry = { @@ -16,6 +17,7 @@ export const entry = { return createError(res, 500, "File Content unavailable: " + fileObj.path, next); } const entries = fileObj.content.entries; + const lastEntry = fileObj.content.entries.at(-1); const entry = {} as Models.IEntry; entry.altitude = Number(req.query.altitude); @@ -25,14 +27,15 @@ export const entry = { entry.lat = Number(req.query.lat); entry.lon = Number(req.query.lon); entry.user = req.query.user as string; - //entry.speed = getSpeed(Number(req.query.speed)) - if (entries.length) { // so there is a previous entry - entry.time = getTime(Number(req.query.timestamp), entries.at(-1)); + if (lastEntry) { // so there is a previous entry + entry.time = getTime(Number(req.query.timestamp), lastEntry); // checkIgnore() // newEntry.angle = getAngle(); - // newEntry.distance = getDistance() + entry.distance = getDistance(entry, lastEntry) + //entry.speed = getSpeed(Number(req.query.speed) , lastEntry); } else { entry.time = getTime(Number(req.query.timestamp)); + //entry.speed = getSpeed(Number(req.query.speed)) } diff --git a/src/scripts/distance.ts b/src/scripts/distance.ts new file mode 100644 index 0000000..79159b8 --- /dev/null +++ b/src/scripts/distance.ts @@ -0,0 +1,30 @@ +export function getDistance(entry: Models.IEntry, lastEntry: Models.IEntry): Models.IDistance { + const horizontal = calculateDistance({ lat: entry.lat, lon: entry.lon }, { lat: lastEntry.lat, lon: lastEntry.lon }); + const vertical = entry.altitude - lastEntry.altitude; + const total = horizontal + Math.abs(vertical); + + return { + horizontal: horizontal, + vertical: vertical, + total: total + } +} + +function toRad(x: number): number { + return x * Math.PI / 180; +} + +function calculateDistance(coord1: { lat: number, lon: number }, coord2: { lat: number, lon: number }): number { + const R = 6371000; // radius of the Earth in meters + const dLat = toRad(coord2.lat - coord1.lat); + const dLon = toRad(coord2.lon - coord1.lon); + + const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(coord1.lat)) * Math.cos(toRad(coord2.lat)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + const distance = R * c; + + return distance; +} \ No newline at end of file diff --git a/src/scripts/time.ts b/src/scripts/time.ts index f31bb39..e15da3e 100644 --- a/src/scripts/time.ts +++ b/src/scripts/time.ts @@ -10,6 +10,7 @@ export function getTime(time: number, entry?: Models.IEntry): Models.ITime { year: "numeric", month: "long", day: "numeric", + hour: '2-digit', hour12: false, minute: '2-digit', }) + ":" + now.getSeconds(); From f8799b2bcd273400dd3f2f5f61ea90fb309d1d4f Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 31 Jan 2024 00:48:53 +0100 Subject: [PATCH 029/206] [Task] #32 writing tests for time and distance --- src/scripts/time.ts | 3 +- src/tests/write.test.ts | 81 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/scripts/time.ts b/src/scripts/time.ts index e15da3e..d4bc6d0 100644 --- a/src/scripts/time.ts +++ b/src/scripts/time.ts @@ -13,7 +13,8 @@ export function getTime(time: number, entry?: Models.IEntry): Models.ITime { hour: '2-digit', hour12: false, minute: '2-digit', - }) + ":" + now.getSeconds(); + second: '2-digit' + }); const diff = entry ? created - entry.time.created : -1; if (uploadDuration < 0) { diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts index 2a61ef8..4154693 100644 --- a/src/tests/write.test.ts +++ b/src/tests/write.test.ts @@ -2,7 +2,7 @@ import axios, { AxiosError } from 'axios'; import fs from "fs"; import path from "path"; -async function callServer(timestamp = new Date().getTime(), query: string, expectStatus: number = 200, method:string = "HEAD") { +async function callServer(timestamp = new Date().getTime(), query: string, expectStatus: number = 200, method: string = "HEAD") { const url = new URL("http://localhost:80/write?"); url.search = "?" + query; const params = new URLSearchParams(url.search); @@ -10,7 +10,7 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec url.search = params.toString(); let response; - if (expectStatus == 200) { + if (expectStatus == 200) { if (method == "GET") { response = await axios.get(url.toString()); } else { @@ -30,7 +30,7 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec describe('HEAD /write', () => { it('with all parameters correctly set it should succeed', async () => { callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200); - }); + }); it('without key it sends 403', async () => { callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0", 403); @@ -72,16 +72,77 @@ describe('HEAD /write', () => { describe("GET /write", () => { + const date = new Date(); + const formattedDate = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; + const dirPath = path.resolve(__dirname, '../../dist/data/'); + const filePath = path.resolve(dirPath, `data-${formattedDate}.json`); + it('there should a file of the current date', async () => { - await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + await callServer(undefined, "user=xx&lat=52.51451&lon=13.35105×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); - const date = new Date(); - const formattedDate = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; - const dirPath = path.resolve(__dirname, '../../dist/data/'); - const filePath = path.resolve(dirPath, `data-${formattedDate}.json`); - console.log(filePath); fs.access(filePath, fs.constants.F_OK, (err) => { expect(err).toBeFalsy(); }); }); -}); \ No newline at end of file + + it('the file contains valid JSON', async () => { + fs.readFile(filePath, 'utf8', (err, data) => { + expect(err).toBeFalsy(); + try { + JSON.parse(data); + } catch (e) { + expect(e).toBeFalsy(); + } + }); + }); + + it('after second call and the JSON entries length is 2', () => { + return new Promise(done => { + // Increase the timeout for this test + setTimeout(async () => { + await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50.0&altitude=5001.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + const data = fs.readFileSync(filePath); + const jsonData = JSON.parse(data.toString()); + + expect(jsonData.entries.length).toBe(2); + + done(); + }, 2000); + }) + }); + + it('the time is correct', () => { + const data = fs.readFileSync(filePath); + const jsonData = JSON.parse(data.toString()); + const lastEntry = jsonData.entries.at(-1) + + expect(lastEntry.time.created).toBeGreaterThan(date.getTime()); + expect(lastEntry.time.diff).toBeGreaterThan(2000); + expect(lastEntry.time.diff).toBeLessThan(3000); + + + const germanDayPattern = "(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)"; + const dayOfMonthPattern = "(0[1-9]|[12][0-9]|3[01])"; + const germanMonthPattern = "(Januar|Februar|März|April|Mai|Juni|Juli|August|September|Oktober|November|Dezember)"; + const yearPattern = "(\\d{4})"; + const timePattern = "([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]"; + + const pattern = new RegExp(`^${germanDayPattern}, ${dayOfMonthPattern}. ${germanMonthPattern} ${yearPattern} um ${timePattern}$`); + const string = lastEntry.time.createdString; + expect(pattern.test(string)).toBeTruthy(); + + }); + + it('the distance is correct', () => { + const data = fs.readFileSync(filePath); + const jsonData = JSON.parse(data.toString()); + const lastEntry = jsonData.entries.at(-1) + + expect(lastEntry.distance.horizontal).toBeCloseTo(1813.926); + expect(lastEntry.distance.vertical).toBe(1); + expect(lastEntry.distance.total).toBeCloseTo(1814.926); + }); + + + +}); From e39b8a7b22714d6399d8eb4eef3209fcd4ee4cd6 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 31 Jan 2024 00:57:43 +0100 Subject: [PATCH 030/206] [Task] #32 change distance calculation to use pythagoras --- src/scripts/distance.ts | 2 +- src/tests/write.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scripts/distance.ts b/src/scripts/distance.ts index 79159b8..36c4637 100644 --- a/src/scripts/distance.ts +++ b/src/scripts/distance.ts @@ -1,7 +1,7 @@ export function getDistance(entry: Models.IEntry, lastEntry: Models.IEntry): Models.IDistance { const horizontal = calculateDistance({ lat: entry.lat, lon: entry.lon }, { lat: lastEntry.lat, lon: lastEntry.lon }); const vertical = entry.altitude - lastEntry.altitude; - const total = horizontal + Math.abs(vertical); + const total = Math.sqrt(horizontal * horizontal + vertical * vertical); return { horizontal: horizontal, diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts index 4154693..741a2d6 100644 --- a/src/tests/write.test.ts +++ b/src/tests/write.test.ts @@ -100,7 +100,7 @@ describe("GET /write", () => { return new Promise(done => { // Increase the timeout for this test setTimeout(async () => { - await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50.0&altitude=5001.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50.0&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); const data = fs.readFileSync(filePath); const jsonData = JSON.parse(data.toString()); @@ -139,8 +139,8 @@ describe("GET /write", () => { const lastEntry = jsonData.entries.at(-1) expect(lastEntry.distance.horizontal).toBeCloseTo(1813.926); - expect(lastEntry.distance.vertical).toBe(1); - expect(lastEntry.distance.total).toBeCloseTo(1814.926); + expect(lastEntry.distance.vertical).toBe(-1000); + expect(lastEntry.distance.total).toBeCloseTo(2071.311); }); From 6393efc21f335a7a4d028135982ad61e0812bdb9 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 31 Jan 2024 01:20:35 +0100 Subject: [PATCH 031/206] [Task] #38 add favicon --- httpdocs/favicon.ico | Bin 0 -> 1150 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 httpdocs/favicon.ico diff --git a/httpdocs/favicon.ico b/httpdocs/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..040a17f731119f6a80ecfd6391ed9bbb0eeb2a0d GIT binary patch literal 1150 zcmbVKOG*Pl5UmhHP}Bqw-I)k3at$w#E4cL*USS}gBe-yFG$`(}&KU$x5Mp4!kn)v1 zJyWSv6R^WeSHDkBb<;#O{Mv28f0ynLh%Shz2Y><-cuYjllH;s%NZE>ABtwNDIT<1U zRIqVfX{E5I$i72nk1Z*Wmf;-Zf9_-M;qt26NPf!`O#bg)hQ%Fue#?3J$XTzqj^5Pl zWi)4VBi!S_ybbL)^~sd0ccirbGf&F*rFdH&BQLm}@!V=ZjJ z=_Gt#_R;zJVB?-kd!&9_aWD4Z&6MR^``Wy$8}C&QdXMJIc28bbbK*b8d5&J0KNftW lHK{)zh#v2Ve$bJ0m1CWEfELgM>cAJUD1dqmZVip$`vtMm3?%>n literal 0 HcmV?d00001 From 353817cf460529d18ba48a1c9dcc2d530ff7035a Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 31 Jan 2024 12:55:44 +0100 Subject: [PATCH 032/206] [Task] #32 time converted to seconds --- src/scripts/time.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scripts/time.ts b/src/scripts/time.ts index d4bc6d0..f6adbc7 100644 --- a/src/scripts/time.ts +++ b/src/scripts/time.ts @@ -4,7 +4,7 @@ export function getTime(time: number, entry?: Models.IEntry): Models.ITime { const now = new Date(); const created = Number(time); const recieved = now.getTime(); - const uploadDuration = recieved - created; + const uploadDuration = (recieved - created) / 1000; const createdString = now.toLocaleString("de-DE", { weekday: "long", year: "numeric", @@ -15,7 +15,7 @@ export function getTime(time: number, entry?: Models.IEntry): Models.ITime { minute: '2-digit', second: '2-digit' }); - const diff = entry ? created - entry.time.created : -1; + const diff = entry ? (created - entry.time.created) / 1000 : undefined; if (uploadDuration < 0) { logger.error(`upload Duration is negative: ${createdString}, index: ${entry ? entry.index + 1 : 0}`); From f7df2d89cb42f203fd1cafa40fc3352396641577 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 31 Jan 2024 12:56:13 +0100 Subject: [PATCH 033/206] [Taskk] #32 speed calculation and output and tests --- src/models/entry.ts | 7 ++++--- src/scripts/speed.ts | 19 +++++++++++++++++++ src/tests/write.test.ts | 13 +++++++++++-- types.d.ts | 8 ++++---- 4 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 src/scripts/speed.ts diff --git a/src/models/entry.ts b/src/models/entry.ts index 2ebe35c..bbbdd75 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -4,7 +4,7 @@ import { crypt } from '@src/scripts/crypt'; import { create as createError } from '@src/error'; import * as file from '@src/scripts/file'; import { getTime } from '@src/scripts/time'; -//import { getSpeed } from '@src/scripts/speed'; +import { getSpeed } from '@src/scripts/speed'; import { getDistance } from '@src/scripts/distance'; @@ -32,10 +32,11 @@ export const entry = { // checkIgnore() // newEntry.angle = getAngle(); entry.distance = getDistance(entry, lastEntry) - //entry.speed = getSpeed(Number(req.query.speed) , lastEntry); + entry.speed = getSpeed(Number(req.query.speed), entry); + } else { entry.time = getTime(Number(req.query.timestamp)); - //entry.speed = getSpeed(Number(req.query.speed)) + entry.speed = getSpeed(Number(req.query.speed)) } diff --git a/src/scripts/speed.ts b/src/scripts/speed.ts new file mode 100644 index 0000000..b0591c0 --- /dev/null +++ b/src/scripts/speed.ts @@ -0,0 +1,19 @@ +export function getSpeed(speed: number, entry?: Models.IEntry): Models.ISpeed { + const gps = speed; + let horizontal; + let vertical; + let total; + + if (entry) { + horizontal = entry.distance.horizontal / entry.time.diff; + vertical = entry.distance.vertical / entry.time.diff; + total = entry.distance.total / entry.time.diff; + } + + return { + gps: gps, + horizontal: horizontal, + vertical: vertical, + total: total + } +} \ No newline at end of file diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts index 741a2d6..d0de6f2 100644 --- a/src/tests/write.test.ts +++ b/src/tests/write.test.ts @@ -117,8 +117,8 @@ describe("GET /write", () => { const lastEntry = jsonData.entries.at(-1) expect(lastEntry.time.created).toBeGreaterThan(date.getTime()); - expect(lastEntry.time.diff).toBeGreaterThan(2000); - expect(lastEntry.time.diff).toBeLessThan(3000); + expect(lastEntry.time.diff).toBeGreaterThan(2); + expect(lastEntry.time.diff).toBeLessThan(3); const germanDayPattern = "(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)"; @@ -143,6 +143,15 @@ describe("GET /write", () => { expect(lastEntry.distance.total).toBeCloseTo(2071.311); }); + it('the speed is correct', () => { + const data = fs.readFileSync(filePath); + const jsonData = JSON.parse(data.toString()); + const lastEntry = jsonData.entries.at(-1) + expect(lastEntry.speed.gps).toBe(150); + expect(lastEntry.speed.horizontal).toBeCloseTo(865.836); + expect(lastEntry.speed.vertical).toBeCloseTo(-477.326); + expect(lastEntry.speed.total).toBeCloseTo(988.69); + }); }); diff --git a/types.d.ts b/types.d.ts index c829a45..a367420 100644 --- a/types.d.ts +++ b/types.d.ts @@ -93,15 +93,15 @@ namespace Models { created: number, recieved: number, uploadDuration: number, - diff: number + diff?: number createdString: string } interface ISpeed { gps: number; - horizontal: number, - vertical: number, - total: number + horizontal?: number, + vertical?: number, + total?: number } interface IDistance { horizontal: number, From 976096a9111319d5553681be76a87d935a626ccd Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 31 Jan 2024 13:47:19 +0100 Subject: [PATCH 034/206] [Task] #32 speed tests --- src/tests/write.test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts index d0de6f2..9b61d7d 100644 --- a/src/tests/write.test.ts +++ b/src/tests/write.test.ts @@ -27,6 +27,10 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec } } +function isInRange(actual:string | number, expected:number, range:number) { + return Math.abs(Number(actual) - expected) <= range; +} + describe('HEAD /write', () => { it('with all parameters correctly set it should succeed', async () => { callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200); @@ -118,7 +122,7 @@ describe("GET /write", () => { expect(lastEntry.time.created).toBeGreaterThan(date.getTime()); expect(lastEntry.time.diff).toBeGreaterThan(2); - expect(lastEntry.time.diff).toBeLessThan(3); + expect(lastEntry.time.diff).toBeLessThan(3); const germanDayPattern = "(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)"; @@ -130,7 +134,7 @@ describe("GET /write", () => { const pattern = new RegExp(`^${germanDayPattern}, ${dayOfMonthPattern}. ${germanMonthPattern} ${yearPattern} um ${timePattern}$`); const string = lastEntry.time.createdString; expect(pattern.test(string)).toBeTruthy(); - + }); it('the distance is correct', () => { @@ -148,10 +152,9 @@ describe("GET /write", () => { const jsonData = JSON.parse(data.toString()); const lastEntry = jsonData.entries.at(-1) - expect(lastEntry.speed.gps).toBe(150); - expect(lastEntry.speed.horizontal).toBeCloseTo(865.836); - expect(lastEntry.speed.vertical).toBeCloseTo(-477.326); - expect(lastEntry.speed.total).toBeCloseTo(988.69); + expect(isInRange(lastEntry.speed.horizontal, 871, 6)).toBe(true); + expect(isInRange(lastEntry.speed.vertical, -479, 6)).toBe(true); + expect(isInRange(lastEntry.speed.total, 995, 6)).toBe(true); }); }); From e9d0bbe66fc2adc5efffeae021ede1c54d799b8e Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 31 Jan 2024 13:52:39 +0100 Subject: [PATCH 035/206] [Task] #33 add ignore --- src/models/entry.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/models/entry.ts b/src/models/entry.ts index bbbdd75..d152c09 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -27,9 +27,10 @@ export const entry = { entry.lat = Number(req.query.lat); entry.lon = Number(req.query.lon); entry.user = req.query.user as string; + entry.ignore = false; if (lastEntry) { // so there is a previous entry entry.time = getTime(Number(req.query.timestamp), lastEntry); - // checkIgnore() + entry.ignore = checkIgnore(lastEntry); // newEntry.angle = getAngle(); entry.distance = getDistance(entry, lastEntry) entry.speed = getSpeed(Number(req.query.speed), entry); @@ -105,6 +106,19 @@ export function checkTime(value: string) { return true } +function checkIgnore(lastEntry: Models.IEntry): boolean { + let threshold = 6; // hdop not allowed to be higher + const maxThreshold = 25; + + // if older the previous entry the higher the threshold + if (lastEntry.time.diff && lastEntry.time.diff > 32) { + threshold += Math.min(lastEntry.time.diff / 60, maxThreshold); + } + + return lastEntry.hdop > threshold; +} + + function checkKey(value: string) { if (process.env.NODE_ENV != "production" && value == "test") { return true; // dev testing convenience @@ -126,4 +140,4 @@ function checkKey(value: string) { } return true; -} \ No newline at end of file +} From e97d965722367ae90e3483eac4e2a2c3944134b7 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 31 Jan 2024 20:13:12 +0100 Subject: [PATCH 036/206] [Task] #32 test finetuning --- src/models/entry.ts | 10 +++++---- src/tests/write.test.ts | 50 +++++++++++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/models/entry.ts b/src/models/entry.ts index d152c09..d2fa351 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -30,7 +30,7 @@ export const entry = { entry.ignore = false; if (lastEntry) { // so there is a previous entry entry.time = getTime(Number(req.query.timestamp), lastEntry); - entry.ignore = checkIgnore(lastEntry); + lastEntry.ignore = checkIgnore(lastEntry, entry); // newEntry.angle = getAngle(); entry.distance = getDistance(entry, lastEntry) entry.speed = getSpeed(Number(req.query.speed), entry); @@ -106,12 +106,14 @@ export function checkTime(value: string) { return true } -function checkIgnore(lastEntry: Models.IEntry): boolean { +function checkIgnore(lastEntry: Models.IEntry, entry:Models.IEntry): boolean { let threshold = 6; // hdop not allowed to be higher const maxThreshold = 25; - // if older the previous entry the higher the threshold - if (lastEntry.time.diff && lastEntry.time.diff > 32) { + const timing = Math.max(lastEntry.time.diff, entry.time.diff) + + // Threshold increases with older previous entries or farther future entries. + if (timing > 32 ) { threshold += Math.min(lastEntry.time.diff / 60, maxThreshold); } diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts index 9b61d7d..0d4971a 100644 --- a/src/tests/write.test.ts +++ b/src/tests/write.test.ts @@ -27,7 +27,7 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec } } -function isInRange(actual:string | number, expected:number, range:number) { +function isInRange(actual: string | number, expected: number, range: number) { return Math.abs(Number(actual) - expected) <= range; } @@ -82,7 +82,7 @@ describe("GET /write", () => { const filePath = path.resolve(dirPath, `data-${formattedDate}.json`); it('there should a file of the current date', async () => { - await callServer(undefined, "user=xx&lat=52.51451&lon=13.35105×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + await callServer(undefined, "user=xx&lat=52.51451&lon=13.35105×tamp=R3Pl4C3&hdop=20.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); fs.access(filePath, fs.constants.F_OK, (err) => { expect(err).toBeFalsy(); @@ -104,7 +104,7 @@ describe("GET /write", () => { return new Promise(done => { // Increase the timeout for this test setTimeout(async () => { - await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50.0&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); const data = fs.readFileSync(filePath); const jsonData = JSON.parse(data.toString()); @@ -118,11 +118,11 @@ describe("GET /write", () => { it('the time is correct', () => { const data = fs.readFileSync(filePath); const jsonData = JSON.parse(data.toString()); - const lastEntry = jsonData.entries.at(-1) + const entry = jsonData.entries.at(-1) - expect(lastEntry.time.created).toBeGreaterThan(date.getTime()); - expect(lastEntry.time.diff).toBeGreaterThan(2); - expect(lastEntry.time.diff).toBeLessThan(3); + expect(entry.time.created).toBeGreaterThan(date.getTime()); + expect(entry.time.diff).toBeGreaterThan(2); + expect(entry.time.diff).toBeLessThan(3); const germanDayPattern = "(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)"; @@ -132,7 +132,7 @@ describe("GET /write", () => { const timePattern = "([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]"; const pattern = new RegExp(`^${germanDayPattern}, ${dayOfMonthPattern}. ${germanMonthPattern} ${yearPattern} um ${timePattern}$`); - const string = lastEntry.time.createdString; + const string = entry.time.createdString; expect(pattern.test(string)).toBeTruthy(); }); @@ -140,21 +140,37 @@ describe("GET /write", () => { it('the distance is correct', () => { const data = fs.readFileSync(filePath); const jsonData = JSON.parse(data.toString()); - const lastEntry = jsonData.entries.at(-1) + const entry = jsonData.entries.at(-1) - expect(lastEntry.distance.horizontal).toBeCloseTo(1813.926); - expect(lastEntry.distance.vertical).toBe(-1000); - expect(lastEntry.distance.total).toBeCloseTo(2071.311); + expect(entry.distance.horizontal).toBeCloseTo(1813.926); + expect(entry.distance.vertical).toBe(-1000); + expect(entry.distance.total).toBeCloseTo(2071.311); }); it('the speed is correct', () => { const data = fs.readFileSync(filePath); const jsonData = JSON.parse(data.toString()); - const lastEntry = jsonData.entries.at(-1) + const entry = jsonData.entries.at(-1) - expect(isInRange(lastEntry.speed.horizontal, 871, 6)).toBe(true); - expect(isInRange(lastEntry.speed.vertical, -479, 6)).toBe(true); - expect(isInRange(lastEntry.speed.total, 995, 6)).toBe(true); + expect(isInRange(entry.speed.horizontal, 870, 10)).toBe(true); + expect(isInRange(entry.speed.vertical, -478, 10)).toBe(true); + expect(isInRange(entry.speed.total, 992, 10)).toBe(true); }); -}); + it('check ignore', async () => { + let data = fs.readFileSync(filePath); + let jsonData = JSON.parse(data.toString()); + let entry = jsonData.entries[1]; + const lastEntry = jsonData.entries[0]; + + expect(entry.ignore).toBe(false); // current one to be false allways + expect(lastEntry.ignore).toBe(true); // last one to high hdop to be true + + await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + data = fs.readFileSync(filePath); // rereading the data + jsonData = JSON.parse(data.toString()); + entry = jsonData.entries[1]; // same data point, but not last now therefore ignore true + expect(entry.ignore).toBe(true); + }); + +}); \ No newline at end of file From 1a89b42591aa424ae7ea71d99793764b4c790d88 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 31 Jan 2024 20:52:21 +0100 Subject: [PATCH 037/206] [Task] #32 add angle between entries --- src/models/entry.ts | 12 +++++++----- src/scripts/angle.ts | 9 +++++++++ types.d.ts | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 src/scripts/angle.ts diff --git a/src/models/entry.ts b/src/models/entry.ts index d2fa351..52d5395 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -6,6 +6,7 @@ import * as file from '@src/scripts/file'; import { getTime } from '@src/scripts/time'; import { getSpeed } from '@src/scripts/speed'; import { getDistance } from '@src/scripts/distance'; +import { getAngle } from '@src/scripts/angle'; export const entry = { @@ -28,14 +29,15 @@ export const entry = { entry.lon = Number(req.query.lon); entry.user = req.query.user as string; entry.ignore = false; + if (lastEntry) { // so there is a previous entry entry.time = getTime(Number(req.query.timestamp), lastEntry); lastEntry.ignore = checkIgnore(lastEntry, entry); - // newEntry.angle = getAngle(); + entry.angle = getAngle(lastEntry, entry); entry.distance = getDistance(entry, lastEntry) entry.speed = getSpeed(Number(req.query.speed), entry); - } else { + entry.angle = undefined; entry.time = getTime(Number(req.query.timestamp)); entry.speed = getSpeed(Number(req.query.speed)) } @@ -106,15 +108,15 @@ export function checkTime(value: string) { return true } -function checkIgnore(lastEntry: Models.IEntry, entry:Models.IEntry): boolean { +function checkIgnore(lastEntry: Models.IEntry, entry: Models.IEntry): boolean { let threshold = 6; // hdop not allowed to be higher const maxThreshold = 25; const timing = Math.max(lastEntry.time.diff, entry.time.diff) // Threshold increases with older previous entries or farther future entries. - if (timing > 32 ) { - threshold += Math.min(lastEntry.time.diff / 60, maxThreshold); + if (timing > 32) { + threshold += Math.min(lastEntry.time.diff / 60, maxThreshold); } return lastEntry.hdop > threshold; diff --git a/src/scripts/angle.ts b/src/scripts/angle.ts new file mode 100644 index 0000000..e6ea972 --- /dev/null +++ b/src/scripts/angle.ts @@ -0,0 +1,9 @@ +export function getAngle(lastEntry: Models.IEntry, entry: Models.IEntry): number { + const dLon = (entry.lon - lastEntry.lon) * Math.PI / 180; + const lat1 = lastEntry.lat * Math.PI / 180; + const lat2 = entry.lat * Math.PI / 180; + const y = Math.sin(dLon) * Math.cos(lat2); + const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon); + const angle = (Math.atan2(y, x) * 180 / Math.PI + 360) % 360; + return angle; +} \ No newline at end of file diff --git a/types.d.ts b/types.d.ts index a367420..7cb4c68 100644 --- a/types.d.ts +++ b/types.d.ts @@ -34,7 +34,7 @@ namespace Models { /** * Direction in degrees between two coordinate pairs: 0°-360° */ - angle: number, + angle?: number, /** * object containing horizontal vertical and total distance, in meters From c346ffb4b24359006f554b6da5abde6ffa29b6a7 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 31 Jan 2024 20:53:27 +0100 Subject: [PATCH 038/206] [Task] #32 test for angle, extracted getData function --- src/tests/write.test.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts index 0d4971a..14d030c 100644 --- a/src/tests/write.test.ts +++ b/src/tests/write.test.ts @@ -27,6 +27,11 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec } } +function getData(filePath:string) { + const data = fs.readFileSync(filePath); + return JSON.parse(data.toString()); +} + function isInRange(actual: string | number, expected: number, range: number) { return Math.abs(Number(actual) - expected) <= range; } @@ -105,8 +110,7 @@ describe("GET /write", () => { // Increase the timeout for this test setTimeout(async () => { await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); - const data = fs.readFileSync(filePath); - const jsonData = JSON.parse(data.toString()); + const jsonData = getData(filePath); expect(jsonData.entries.length).toBe(2); @@ -116,8 +120,7 @@ describe("GET /write", () => { }); it('the time is correct', () => { - const data = fs.readFileSync(filePath); - const jsonData = JSON.parse(data.toString()); + const jsonData = getData(filePath); const entry = jsonData.entries.at(-1) expect(entry.time.created).toBeGreaterThan(date.getTime()); @@ -138,8 +141,7 @@ describe("GET /write", () => { }); it('the distance is correct', () => { - const data = fs.readFileSync(filePath); - const jsonData = JSON.parse(data.toString()); + const jsonData = getData(filePath); const entry = jsonData.entries.at(-1) expect(entry.distance.horizontal).toBeCloseTo(1813.926); @@ -147,9 +149,15 @@ describe("GET /write", () => { expect(entry.distance.total).toBeCloseTo(2071.311); }); + it('the angle is correct', () => { + const jsonData = getData(filePath); + const entry = jsonData.entries.at(-1) + + expect(entry.angle).toBeCloseTo(83.795775); + }); + it('the speed is correct', () => { - const data = fs.readFileSync(filePath); - const jsonData = JSON.parse(data.toString()); + const jsonData = getData(filePath); const entry = jsonData.entries.at(-1) expect(isInRange(entry.speed.horizontal, 870, 10)).toBe(true); @@ -158,8 +166,7 @@ describe("GET /write", () => { }); it('check ignore', async () => { - let data = fs.readFileSync(filePath); - let jsonData = JSON.parse(data.toString()); + let jsonData = getData(filePath); let entry = jsonData.entries[1]; const lastEntry = jsonData.entries[0]; @@ -167,8 +174,7 @@ describe("GET /write", () => { expect(lastEntry.ignore).toBe(true); // last one to high hdop to be true await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); - data = fs.readFileSync(filePath); // rereading the data - jsonData = JSON.parse(data.toString()); + jsonData = getData(filePath); entry = jsonData.entries[1]; // same data point, but not last now therefore ignore true expect(entry.ignore).toBe(true); }); From 003e57d1ffd932bc9182d88fbb5f70e2882ccbec Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 1 Feb 2024 12:35:31 +0100 Subject: [PATCH 039/206] [change] #32 test to include optional leading 0 for days --- src/tests/write.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts index 14d030c..69f85f2 100644 --- a/src/tests/write.test.ts +++ b/src/tests/write.test.ts @@ -27,7 +27,7 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec } } -function getData(filePath:string) { +function getData(filePath: string) { const data = fs.readFileSync(filePath); return JSON.parse(data.toString()); } @@ -129,11 +129,10 @@ describe("GET /write", () => { const germanDayPattern = "(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)"; - const dayOfMonthPattern = "(0[1-9]|[12][0-9]|3[01])"; + const dayOfMonthPattern = "(0?[1-9]|[12][0-9]|3[01])"; const germanMonthPattern = "(Januar|Februar|März|April|Mai|Juni|Juli|August|September|Oktober|November|Dezember)"; const yearPattern = "(\\d{4})"; const timePattern = "([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]"; - const pattern = new RegExp(`^${germanDayPattern}, ${dayOfMonthPattern}. ${germanMonthPattern} ${yearPattern} um ${timePattern}$`); const string = entry.time.createdString; expect(pattern.test(string)).toBeTruthy(); @@ -179,4 +178,11 @@ describe("GET /write", () => { expect(entry.ignore).toBe(true); }); + /* it('can handle up to 500 lines', async () => { + for (let i = 0; i <= 500; i++) { + await callServer(undefined, `user=xx&lat=${52 + Math.random()}&lon=${13 + Math.random()}×tamp=R3Pl4C3&hdop=${25 * Math.random()}&altitude=${i}&speed=150.000&heading=${360 * Math.random()}&key=test`, 200, "GET"); + + } + }); */ + }); \ No newline at end of file From dbbbb4615bbfc6888d837d6cfe3d07874c318824 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 1 Feb 2024 15:05:22 +0100 Subject: [PATCH 040/206] [!!!Task] #18 add uncaughtExeption handler as last resort --- src/app.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app.ts b/src/app.ts index d65799a..8dbc27d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -42,4 +42,10 @@ app.use(error.handler); // init server app.listen(80, () => { logger.log(`Server running //localhost:80, ENV: ${process.env.NODE_ENV}`, true); +}); + +process.on('uncaughtException', function(err) { + console.error('Caught exception:', err); + logger.error(err); + process.exit(1); }); \ No newline at end of file From 50c97004ff1b22132507d6d3751e026477777ea3 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 2 Feb 2024 13:17:35 +0100 Subject: [PATCH 041/206] [Task] #7 enhance static options to include common filetypes; index file start is used as index file to avoid collisions with host provider --- src/app.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app.ts b/src/app.ts index 8dbc27d..d7fc161 100644 --- a/src/app.ts +++ b/src/app.ts @@ -18,9 +18,9 @@ app.use( directives: { "default-src": "'self'", "img-src": "*" - }, - }, - }), + } + } + }) ); app.use(hpp()); @@ -33,7 +33,10 @@ app.get('/', (req, res) => { app.use('/write', writeRouter); // use httpdocs as static folder -app.use('/', express.static(path.join(__dirname, 'httpdocs'))) +app.use('/', express.static(path.join(__dirname, 'httpdocs'), { + extensions: ['html', 'txt', "pdf"], + index: "start.html", +})) // error handling app.use(error.notFound); From 04e276175b9441eebdef94ad1a2e4d57c1f30d04 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 2 Feb 2024 13:36:00 +0100 Subject: [PATCH 042/206] [change] #32 validation to be used more explictly --- src/controller/write.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/controller/write.ts b/src/controller/write.ts index e4c6ef0..dc0658d 100644 --- a/src/controller/write.ts +++ b/src/controller/write.ts @@ -36,9 +36,7 @@ async function errorChecking (req:Request, res:Response, next:NextFunction) { const router = express.Router(); -router.use(entry.validate); - -router.get('/', errorChecking); -router.head('/', errorChecking); +router.get('/', entry.validate, errorChecking); +router.head('/', entry.validate, errorChecking); export default router; \ No newline at end of file From c0b9db6ff60cca7040ec28651981f6dcefda59cc Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 2 Feb 2024 13:36:30 +0100 Subject: [PATCH 043/206] [change] #32 add index to log while writing --- src/scripts/file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/file.ts b/src/scripts/file.ts index 5f0cbe5..5fdec3f 100644 --- a/src/scripts/file.ts +++ b/src/scripts/file.ts @@ -53,7 +53,7 @@ export const write = (res:Response, fileObj:File.Obj, next: NextFunction) => { const content = JSON.stringify(fileObj.content, undefined, 2); fs.writeFileSync(fileObj.path, content); fileObj.content = JSON.parse(content); - logger.log(`written to file: ${fileObj.path}`); + logger.log(`written to file: ${fileObj.path} ${fileObj.content ? fileObj.content?.entries.length - 1 : ''}`); } catch (err) { createError(res, 500, `File (${fileObj.path}) cannot be written to`, next); } From 29cc03c0f6fb158190d0e2252d19f6fec937472d Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 2 Feb 2024 13:37:49 +0100 Subject: [PATCH 044/206] [Task] #32 test if 1000 calls can be made with randomized data --- src/tests/write.test.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts index 69f85f2..e904466 100644 --- a/src/tests/write.test.ts +++ b/src/tests/write.test.ts @@ -161,7 +161,7 @@ describe("GET /write", () => { expect(isInRange(entry.speed.horizontal, 870, 10)).toBe(true); expect(isInRange(entry.speed.vertical, -478, 10)).toBe(true); - expect(isInRange(entry.speed.total, 992, 10)).toBe(true); + expect(isInRange(entry.speed.total, 992, 15)).toBe(true); }); it('check ignore', async () => { @@ -176,13 +176,17 @@ describe("GET /write", () => { jsonData = getData(filePath); entry = jsonData.entries[1]; // same data point, but not last now therefore ignore true expect(entry.ignore).toBe(true); - }); - - /* it('can handle up to 500 lines', async () => { - for (let i = 0; i <= 500; i++) { - await callServer(undefined, `user=xx&lat=${52 + Math.random()}&lon=${13 + Math.random()}×tamp=R3Pl4C3&hdop=${25 * Math.random()}&altitude=${i}&speed=150.000&heading=${360 * Math.random()}&key=test`, 200, "GET"); + }); +}); +describe('API calls', () => { + test(`1000 api calls`, async () => { + for (let i = 0; i < 1000; i++) { + const url = `http://localhost:80/write?user=xx&lat=${(52 + Math.random()).toFixed(3)}&lon=${(13 + Math.random()).toFixed(3)}×tamp=${new Date().getTime()}&hdop=${(25 * Math.random()).toFixed(3)}&altitude=${i}&speed=88.888&heading=${(360 * Math.random()).toFixed(3)}&key=test`; + const response = await axios.get(url); + expect(response.status).toBe(200); } - }); */ + }, 20000); // adjust this to to fit your setup + }); \ No newline at end of file From 1145e9e85c2aef53671cf431a1deb8b79d537b20 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 2 Feb 2024 13:39:01 +0100 Subject: [PATCH 045/206] [!!! Task] #32 limit JSON Data to be 1000 lines: replace last line with most recent entry --- src/models/entry.ts | 10 ++++++++-- src/tests/write.test.ts | 8 ++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/models/entry.ts b/src/models/entry.ts index 52d5395..7bee7c8 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -7,6 +7,7 @@ import { getTime } from '@src/scripts/time'; import { getSpeed } from '@src/scripts/speed'; import { getDistance } from '@src/scripts/distance'; import { getAngle } from '@src/scripts/angle'; +import logger from '@src/scripts/logger'; export const entry = { @@ -42,8 +43,13 @@ export const entry = { entry.speed = getSpeed(Number(req.query.speed)) } - - entries.push(entry); + if (entries.length >= 1000) { + logger.log(`File over 1000 lines: ${fileObj.path}`); + entries[entries.length - 1] = entry; // replace last entry + } else { + entries.push(entry); + } + file.write(res, fileObj, next); diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts index e904466..5db4a87 100644 --- a/src/tests/write.test.ts +++ b/src/tests/write.test.ts @@ -188,5 +188,13 @@ describe('API calls', () => { } }, 20000); // adjust this to to fit your setup + test(`length of json should not exceed 1000`, async () => { + const date = new Date(); + const formattedDate = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; + const dirPath = path.resolve(__dirname, '../../dist/data/'); + const filePath = path.resolve(dirPath, `data-${formattedDate}.json`); + const jsonData = getData(filePath); + expect(jsonData.entries.length).toBeLessThanOrEqual(1000); + }); }); \ No newline at end of file From 19aa8eb522b2a10ea151582a1dee9c49af296dbf Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 2 Feb 2024 14:01:42 +0100 Subject: [PATCH 046/206] [Change, Task] #32 if 1000 entries exceeded, only replace last if hdop is good --- src/models/entry.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/models/entry.ts b/src/models/entry.ts index 7bee7c8..147256e 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -45,11 +45,12 @@ export const entry = { if (entries.length >= 1000) { logger.log(`File over 1000 lines: ${fileObj.path}`); - entries[entries.length - 1] = entry; // replace last entry + if (entry.hdop < 12 || (lastEntry && entry.hdop < lastEntry.hdop)) { + entries[entries.length - 1] = entry; // replace last entry + } } else { entries.push(entry); - } - + } file.write(res, fileObj, next); From e1d6827aa86748759d2673dc137c54153ae60347 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 2 Feb 2024 15:07:07 +0100 Subject: [PATCH 047/206] [Change] build action enable button to on manually --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d0f1e0..72c45e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,7 @@ name: Node.js CI on: + workflow_dispatch: push: branches: [ "main", "dev" ] pull_request: From df0711ce64f4d7e72798a62ff5c1ed5622443250 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 2 Feb 2024 14:25:04 +0100 Subject: [PATCH 048/206] [temp] test y tests fail --- .github/workflows/build.yml | 2 +- package.json | 2 +- src/app.ts | 26 +++-------------------- src/tests/app.test.ts | 2 +- src/tests/write.test.ts | 41 ++++++++++++++++++------------------- 5 files changed, 26 insertions(+), 47 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d0f1e0..f92eb6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,5 +21,5 @@ jobs: - name: Start server run: | npm start & - sleep 5 # Give server some time to start + sleep 10 # Give server some time to start - run: npm test diff --git a/package.json b/package.json index 7e450e9..86d5cdc 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "start": "node dist/app.js", "dev": "rm -rf dist/* && cp -R httpdocs/ dist/ && nodemon src/app.ts", "lint": "eslint . --fix", - "test": "jest" + "test": "jest --runInBand" }, "keywords": [], "author": "Type-Style", diff --git a/src/app.ts b/src/app.ts index d7fc161..382c255 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,7 +1,6 @@ require('module-alias/register'); import { config } from 'dotenv'; import express from 'express'; -import helmet from 'helmet'; import hpp from 'hpp'; import cache from './cache'; import * as error from "./error"; @@ -9,20 +8,10 @@ import writeRouter from '@src/controller/write'; import path from 'path'; import logger from '@src/scripts/logger'; + // configurations config(); const app = express(); -app.use( - helmet({ - contentSecurityPolicy: { - directives: { - "default-src": "'self'", - "img-src": "*" - } - } - }) -); - app.use(hpp()); app.use(cache); @@ -33,10 +22,7 @@ app.get('/', (req, res) => { app.use('/write', writeRouter); // use httpdocs as static folder -app.use('/', express.static(path.join(__dirname, 'httpdocs'), { - extensions: ['html', 'txt', "pdf"], - index: "start.html", -})) +app.use('/', express.static(path.join(__dirname, 'httpdocs'))) // error handling app.use(error.notFound); @@ -44,11 +30,5 @@ app.use(error.handler); // init server app.listen(80, () => { - logger.log(`Server running //localhost:80, ENV: ${process.env.NODE_ENV}`, true); -}); - -process.on('uncaughtException', function(err) { - console.error('Caught exception:', err); - logger.error(err); - process.exit(1); + logger.log(`Server running //localhost:80`); }); \ No newline at end of file diff --git a/src/tests/app.test.ts b/src/tests/app.test.ts index 453caf4..b7c01c2 100644 --- a/src/tests/app.test.ts +++ b/src/tests/app.test.ts @@ -4,7 +4,7 @@ describe('Server Status', () => { it('The server is running', async () => { let serverStatus; try { - const response = await axios.get('http://localhost:80'); + const response = await axios.get('http://localhost:80/'); serverStatus = response.status; } catch (error) { console.error(error); diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts index 5db4a87..e5b4a29 100644 --- a/src/tests/write.test.ts +++ b/src/tests/write.test.ts @@ -1,6 +1,6 @@ import axios, { AxiosError } from 'axios'; import fs from "fs"; -import path from "path"; +// import path from "path"; async function callServer(timestamp = new Date().getTime(), query: string, expectStatus: number = 200, method: string = "HEAD") { const url = new URL("http://localhost:80/write?"); @@ -27,59 +27,59 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec } } -function getData(filePath: string) { +/* function getData(filePath: string) { const data = fs.readFileSync(filePath); return JSON.parse(data.toString()); } function isInRange(actual: string | number, expected: number, range: number) { return Math.abs(Number(actual) - expected) <= range; -} +} */ describe('HEAD /write', () => { it('with all parameters correctly set it should succeed', async () => { - callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200); + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200); }); it('without key it sends 403', async () => { - callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0", 403); + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0", 403); }); it('with user length not equal to 2 it sends 422', async () => { - callServer(undefined, "user=x&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + await callServer(undefined, "user=x&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); }); it('with lat not between -90 and 90 it sends 422', async () => { - callServer(undefined, "user=xx&lat=91.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + await callServer(undefined, "user=xx&lat=91.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); }); it('with lon not between -180 and 180 it sends 422', async () => { - callServer(undefined, "user=xx&lat=45.000&lon=181.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + await callServer(undefined, "user=xx&lat=45.000&lon=181.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); }); it('with timestamp to old sends 422', async () => { const timestamp = new Date().getTime() - 24 * 60 * 60 * 1000 * 2; // two days ago - callServer(timestamp, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + await callServer(timestamp, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); }) it('with hdop not between 0 and 100 it sends 422', async () => { - callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); }); it('with altitude not between 0 and 10000 it sends 422', async () => { - callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test", 422); + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test", 422); }); it('with speed not between 0 and 300 it sends 422', async () => { - callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test", 422); + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test", 422); }); it('with heading not between 0 and 360 it sends 422', async () => { - callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test", 422); + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test", 422); }); }); - +/* describe("GET /write", () => { const date = new Date(); const formattedDate = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; @@ -87,7 +87,7 @@ describe("GET /write", () => { const filePath = path.resolve(dirPath, `data-${formattedDate}.json`); it('there should a file of the current date', async () => { - await callServer(undefined, "user=xx&lat=52.51451&lon=13.35105×tamp=R3Pl4C3&hdop=20.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + await await callServer(undefined, "user=xx&lat=52.51451&lon=13.35105×tamp=R3Pl4C3&hdop=20.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); fs.access(filePath, fs.constants.F_OK, (err) => { expect(err).toBeFalsy(); @@ -109,7 +109,7 @@ describe("GET /write", () => { return new Promise(done => { // Increase the timeout for this test setTimeout(async () => { - await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + await await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); const jsonData = getData(filePath); expect(jsonData.entries.length).toBe(2); @@ -172,14 +172,14 @@ describe("GET /write", () => { expect(entry.ignore).toBe(false); // current one to be false allways expect(lastEntry.ignore).toBe(true); // last one to high hdop to be true - await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + await await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); jsonData = getData(filePath); entry = jsonData.entries[1]; // same data point, but not last now therefore ignore true expect(entry.ignore).toBe(true); }); -}); +}); */ -describe('API calls', () => { +/* describe('API calls', () => { test(`1000 api calls`, async () => { for (let i = 0; i < 1000; i++) { const url = `http://localhost:80/write?user=xx&lat=${(52 + Math.random()).toFixed(3)}&lon=${(13 + Math.random()).toFixed(3)}×tamp=${new Date().getTime()}&hdop=${(25 * Math.random()).toFixed(3)}&altitude=${i}&speed=88.888&heading=${(360 * Math.random()).toFixed(3)}&key=test`; @@ -196,5 +196,4 @@ describe('API calls', () => { const jsonData = getData(filePath); expect(jsonData.entries.length).toBeLessThanOrEqual(1000); }); - -}); \ No newline at end of file +}); */ \ No newline at end of file From 9702002fe5e4214ffbb95d604b469e2c28cdd5bb Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 5 Feb 2024 14:18:36 +0100 Subject: [PATCH 049/206] Create node.js.yml --- .github/workflows/node.js.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/node.js.yml diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..a38f85e --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,34 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Node (2) + +on: + workflow_dispatch: + push: + branches: [ "dev" ] + pull_request: + branches: [ "dev" ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run build --if-present + - name: Start server + run: | + npm start & + sleep 6 # Give server some time to start + - run: npm test From 32d5579bafee315af564883e9aeb14898bc432a0 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 5 Feb 2024 14:38:28 +0100 Subject: [PATCH 050/206] Create main.yml --- .github/workflows/main.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..b00a829 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,31 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Node3 + +on: + workflow_dispatch: + push: + branches: [ "dev", "main" ] + pull_request: + branches: [ "dev", "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: '20', + cache: 'npm' + - run: npm ci + - run: npm run build --if-present + - name: Start server + run: | + npm start & + sleep 6 # Give server some time to start + - run: npm test From 0ca2a85fd4ba3813c3df906e26ea01754840e2de Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 5 Feb 2024 14:40:31 +0100 Subject: [PATCH 051/206] [!!!Fix] Created new workflow to build / test node, commented tests back in. Increased time between server calls in test, to check difference time more accurately --- .github/workflows/main.yml | 13 ++++--- package.json | 2 +- src/tests/write.test.ts | 79 +++++++++++++++++++------------------- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b00a829..a755f3f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,3 @@ -# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs - name: Node3 on: @@ -20,12 +17,16 @@ jobs: - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: - node-version: '20', + node-version: '20' cache: 'npm' - run: npm ci - run: npm run build --if-present - name: Start server run: | - npm start & + sudo npm start & sleep 6 # Give server some time to start - - run: npm test + - name: Check if server is running + run: | + curl --fail http://localhost:80 || exit 1 + - name: Run tests + run: npm run test \ No newline at end of file diff --git a/package.json b/package.json index 227ed1f..7e450e9 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "start": "node dist/app.js", "dev": "rm -rf dist/* && cp -R httpdocs/ dist/ && nodemon src/app.ts", "lint": "eslint . --fix", - "test": "jest --maxWorkers=1" + "test": "jest" }, "keywords": [], "author": "Type-Style", diff --git a/src/tests/write.test.ts b/src/tests/write.test.ts index 507781e..b7bd9bc 100644 --- a/src/tests/write.test.ts +++ b/src/tests/write.test.ts @@ -1,6 +1,6 @@ import axios, { AxiosError } from 'axios'; -//import fs from "fs"; -// import path from "path"; +import fs from "fs"; +import path from "path"; async function callServer(timestamp = new Date().getTime(), query: string, expectStatus: number = 200, method: string = "HEAD") { const url = new URL("http://localhost:80/write?"); @@ -32,59 +32,59 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec } - /* function getData(filePath: string) { + function getData(filePath: string) { const data = fs.readFileSync(filePath); return JSON.parse(data.toString()); } function isInRange(actual: string | number, expected: number, range: number) { return Math.abs(Number(actual) - expected) <= range; - } */ + } describe('HEAD /write', () => { it('with all parameters correctly set it should succeed', async () => { await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200); }); - // it('without key it sends 403', async () => { - // await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0", 403); - // }); + it('without key it sends 403', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0", 403); + }); it('with user length not equal to 2 it sends 422', async () => { await callServer(undefined, "user=x&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); }); - // it('with lat not between -90 and 90 it sends 422', async () => { - // await callServer(undefined, "user=xx&lat=91.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); - // }); + it('with lat not between -90 and 90 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=91.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }); - // it('with lon not between -180 and 180 it sends 422', async () => { - // await callServer(undefined, "user=xx&lat=45.000&lon=181.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); - // }); + it('with lon not between -180 and 180 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=181.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }); - // it('with timestamp to old sends 422', async () => { - // const timestamp = new Date().getTime() - 24 * 60 * 60 * 1000 * 2; // two days ago - // await callServer(timestamp, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); - // }) + it('with timestamp to old sends 422', async () => { + const timestamp = new Date().getTime() - 24 * 60 * 60 * 1000 * 2; // two days ago + await callServer(timestamp, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }) - // it('with hdop not between 0 and 100 it sends 422', async () => { - // await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); - // }); + it('with hdop not between 0 and 100 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }); - // it('with altitude not between 0 and 10000 it sends 422', async () => { - // await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test", 422); - // }); + it('with altitude not between 0 and 10000 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test", 422); + }); - // it('with speed not between 0 and 300 it sends 422', async () => { - // await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test", 422); - // }); + it('with speed not between 0 and 300 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test", 422); + }); - // it('with heading not between 0 and 360 it sends 422', async () => { - // await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test", 422); - // }); + it('with heading not between 0 and 360 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test", 422); + }); }); -/* + describe("GET /write", () => { const date = new Date(); const formattedDate = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; @@ -112,7 +112,6 @@ describe("GET /write", () => { it('after second call and the JSON entries length is 2', () => { return new Promise(done => { - // Increase the timeout for this test setTimeout(async () => { await await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); const jsonData = getData(filePath); @@ -120,7 +119,7 @@ describe("GET /write", () => { expect(jsonData.entries.length).toBe(2); done(); - }, 2000); + }, 3500); }) }); @@ -129,8 +128,8 @@ describe("GET /write", () => { const entry = jsonData.entries.at(-1) expect(entry.time.created).toBeGreaterThan(date.getTime()); - expect(entry.time.diff).toBeGreaterThan(2); - expect(entry.time.diff).toBeLessThan(3); + expect(entry.time.diff).toBeGreaterThan(3.5); + expect(entry.time.diff).toBeLessThan(4); const germanDayPattern = "(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)"; @@ -164,9 +163,9 @@ describe("GET /write", () => { const jsonData = getData(filePath); const entry = jsonData.entries.at(-1) - expect(isInRange(entry.speed.horizontal, 870, 10)).toBe(true); - expect(isInRange(entry.speed.vertical, -478, 10)).toBe(true); - expect(isInRange(entry.speed.total, 992, 15)).toBe(true); + expect(isInRange(entry.speed.horizontal, 515, 10)).toBe(true); + expect(isInRange(entry.speed.vertical, -284, 10)).toBe(true); + expect(isInRange(entry.speed.total, 588, 15)).toBe(true); }); it('check ignore', async () => { @@ -182,9 +181,9 @@ describe("GET /write", () => { entry = jsonData.entries[1]; // same data point, but not last now therefore ignore true expect(entry.ignore).toBe(true); }); -}); */ +}); -/* describe('API calls', () => { +describe('API calls', () => { test(`1000 api calls`, async () => { for (let i = 0; i < 1000; i++) { const url = `http://localhost:80/write?user=xx&lat=${(52 + Math.random()).toFixed(3)}&lon=${(13 + Math.random()).toFixed(3)}×tamp=${new Date().getTime()}&hdop=${(25 * Math.random()).toFixed(3)}&altitude=${i}&speed=88.888&heading=${(360 * Math.random()).toFixed(3)}&key=test`; @@ -201,4 +200,4 @@ describe("GET /write", () => { const jsonData = getData(filePath); expect(jsonData.entries.length).toBeLessThanOrEqual(1000); }); -}); */ \ No newline at end of file +}); \ No newline at end of file From 8fb105755fdb9c1ff718369dd1f5fde341245383 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 6 Feb 2024 15:03:24 +0100 Subject: [PATCH 052/206] [Task] #33 moved ignore to its own file since it creates data rather than validating it --- src/models/entry.ts | 17 ++--------------- src/scripts/ignore.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 src/scripts/ignore.ts diff --git a/src/models/entry.ts b/src/models/entry.ts index 147256e..4305fea 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -7,6 +7,7 @@ import { getTime } from '@src/scripts/time'; import { getSpeed } from '@src/scripts/speed'; import { getDistance } from '@src/scripts/distance'; import { getAngle } from '@src/scripts/angle'; +import { getIgnore } from '@src/scripts/ignore'; import logger from '@src/scripts/logger'; @@ -33,7 +34,7 @@ export const entry = { if (lastEntry) { // so there is a previous entry entry.time = getTime(Number(req.query.timestamp), lastEntry); - lastEntry.ignore = checkIgnore(lastEntry, entry); + lastEntry.ignore = getIgnore(lastEntry, entry); entry.angle = getAngle(lastEntry, entry); entry.distance = getDistance(entry, lastEntry) entry.speed = getSpeed(Number(req.query.speed), entry); @@ -115,20 +116,6 @@ export function checkTime(value: string) { return true } -function checkIgnore(lastEntry: Models.IEntry, entry: Models.IEntry): boolean { - let threshold = 6; // hdop not allowed to be higher - const maxThreshold = 25; - - const timing = Math.max(lastEntry.time.diff, entry.time.diff) - - // Threshold increases with older previous entries or farther future entries. - if (timing > 32) { - threshold += Math.min(lastEntry.time.diff / 60, maxThreshold); - } - - return lastEntry.hdop > threshold; -} - function checkKey(value: string) { if (process.env.NODE_ENV != "production" && value == "test") { diff --git a/src/scripts/ignore.ts b/src/scripts/ignore.ts new file mode 100644 index 0000000..3e92857 --- /dev/null +++ b/src/scripts/ignore.ts @@ -0,0 +1,13 @@ +export function getIgnore(lastEntry: Models.IEntry, entry: Models.IEntry): boolean { + let threshold = 6; // hdop not allowed to be higher + const maxThreshold = 25; + + const timing = Math.max(lastEntry.time.diff, entry.time.diff) + + // Threshold increases with older previous entries or farther future entries. + if (timing > 32) { + threshold += Math.min(lastEntry.time.diff / 60, maxThreshold); + } + + return lastEntry.hdop > threshold; +} \ No newline at end of file From 5ddf95158447761743e0b29bd425a85c0c292218 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 6 Feb 2024 23:43:29 +0100 Subject: [PATCH 053/206] 42 output json (#44) * [Task] #42, created route to output json * [Task] #42 added tests for read json --- jest.config.js | 1 + src/app.ts | 7 +- src/controller/read.ts | 34 +++++ .../{write.test.ts => integration.test.ts} | 121 ++++++++++++------ 4 files changed, 117 insertions(+), 46 deletions(-) create mode 100644 src/controller/read.ts rename src/tests/{write.test.ts => integration.test.ts} (59%) diff --git a/jest.config.js b/jest.config.js index 2adbbae..0a57f54 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,4 +6,5 @@ module.exports = { moduleNameMapper: { '^@src/(.*)$': '/src/$1', }, + bail: true }; \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 0aaaa42..0f5b50a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,6 +6,7 @@ import hpp from 'hpp'; import cache from './cache'; import * as error from "./error"; import writeRouter from '@src/controller/write'; +import readRouter from '@src/controller/read'; import path from 'path'; import logger from '@src/scripts/logger'; @@ -31,11 +32,9 @@ app.get('/', (req, res) => { res.send('Hello World, via TypeScript and Node.js!'); }); -app.get('/test', (req, res) => { - process.exit(1); - res.send('Hello World 2, via TypeScript and Node.js!'); -}); + app.use('/write', writeRouter); +app.use('/read', readRouter); // use httpdocs as static folder app.use('/', express.static(path.join(__dirname, 'httpdocs'), { diff --git a/src/controller/read.ts b/src/controller/read.ts new file mode 100644 index 0000000..f17c93c --- /dev/null +++ b/src/controller/read.ts @@ -0,0 +1,34 @@ +import express, { Request, Response, NextFunction } from 'express'; +import * as file from '@src/scripts/file'; +import { create as createError } from '@src/error'; +import { validationResult, query } from 'express-validator'; + +const router = express.Router(); + +router.get('/', + [query('index').isInt().withMessage("not an integer") + .isLength({ max: 3 }).withMessage("not in range") + .toInt()], + async function getRead(req:Request, res:Response, next:NextFunction) { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return createError(res, 400, JSON.stringify({errors: errors.array()}), next) + } + + const fileObj: File.Obj = file.getFile(res, next); + fileObj.content = await file.readAsJson(res, fileObj.path, next) + if (!fileObj.content || !Array.isArray(fileObj.content.entries)) { + return createError(res, undefined, `File corrupt: ${fileObj.path}`, next); + } + + let entries = fileObj.content.entries; + + if (req.query.index) { + entries = entries.slice(Number(req.query.index)); + } + + res.json({entries}); +}); + + +export default router; \ No newline at end of file diff --git a/src/tests/write.test.ts b/src/tests/integration.test.ts similarity index 59% rename from src/tests/write.test.ts rename to src/tests/integration.test.ts index b7bd9bc..2d40ced 100644 --- a/src/tests/write.test.ts +++ b/src/tests/integration.test.ts @@ -32,57 +32,57 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec } - function getData(filePath: string) { - const data = fs.readFileSync(filePath); - return JSON.parse(data.toString()); - } - - function isInRange(actual: string | number, expected: number, range: number) { - return Math.abs(Number(actual) - expected) <= range; - } +function getData(filePath: string) { + const data = fs.readFileSync(filePath); + return JSON.parse(data.toString()); +} + +function isInRange(actual: string | number, expected: number, range: number) { + return Math.abs(Number(actual) - expected) <= range; +} - describe('HEAD /write', () => { - it('with all parameters correctly set it should succeed', async () => { - await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200); - }); +describe('HEAD /write', () => { + it('with all parameters correctly set it should succeed', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200); + }); - it('without key it sends 403', async () => { - await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0", 403); - }); + it('without key it sends 403', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0", 403); + }); - it('with user length not equal to 2 it sends 422', async () => { - await callServer(undefined, "user=x&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); - }); + it('with user length not equal to 2 it sends 422', async () => { + await callServer(undefined, "user=x&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }); - it('with lat not between -90 and 90 it sends 422', async () => { - await callServer(undefined, "user=xx&lat=91.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); - }); + it('with lat not between -90 and 90 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=91.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }); - it('with lon not between -180 and 180 it sends 422', async () => { - await callServer(undefined, "user=xx&lat=45.000&lon=181.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); - }); + it('with lon not between -180 and 180 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=181.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }); - it('with timestamp to old sends 422', async () => { - const timestamp = new Date().getTime() - 24 * 60 * 60 * 1000 * 2; // two days ago - await callServer(timestamp, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); - }) + it('with timestamp to old sends 422', async () => { + const timestamp = new Date().getTime() - 24 * 60 * 60 * 1000 * 2; // two days ago + await callServer(timestamp, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }) - it('with hdop not between 0 and 100 it sends 422', async () => { - await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); - }); + it('with hdop not between 0 and 100 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); + }); - it('with altitude not between 0 and 10000 it sends 422', async () => { - await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test", 422); - }); + it('with altitude not between 0 and 10000 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test", 422); + }); - it('with speed not between 0 and 300 it sends 422', async () => { - await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test", 422); - }); + it('with speed not between 0 and 300 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test", 422); + }); - it('with heading not between 0 and 360 it sends 422', async () => { - await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test", 422); - }); - }); + it('with heading not between 0 and 360 it sends 422', async () => { + await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test", 422); + }); +}); describe("GET /write", () => { @@ -180,7 +180,7 @@ describe("GET /write", () => { jsonData = getData(filePath); entry = jsonData.entries[1]; // same data point, but not last now therefore ignore true expect(entry.ignore).toBe(true); - }); + }); }); describe('API calls', () => { @@ -200,4 +200,41 @@ describe('API calls', () => { const jsonData = getData(filePath); expect(jsonData.entries.length).toBeLessThanOrEqual(1000); }); +}); + + +describe('/read', () => { + test(`returns json`, async () => { + const response = await axios.get("http://localhost:80/read?index=0"); + expect(response.status).toBe(200); + expect(response.headers['content-type']).toEqual(expect.stringContaining('application/json')); + }); + test(`index parameter to long`, async () => { + try { + await axios.get("http://localhost:80/read?index=1234"); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + expect(axiosError.response.status).toBe(400); + } else { + console.error(axiosError); + } + } + }); + test(`index parameter to be a number`, async () => { + try { + await axios.get("http://localhost:80/read?index=a9"); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + expect(axiosError.response.status).toBe(400); + } else { + console.error(axiosError); + } + } + }); + test(`index parameter reduces length of json`, async () => { + const response = await axios.get("http://localhost:80/read?index=999"); + expect(response.data.entries.length).toBe(1); + }); }); \ No newline at end of file From f1a628c79f193fe9b6d7d631b7cbccfb227e6818 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 13 Feb 2024 19:01:19 +0100 Subject: [PATCH 054/206] 41 add rate limiter (#45) * [Task] #18, limit request size for security reasons * [Task] #43, introduce gzip to transfer data * [Task] #34 improve error handling, log server shutdowns * [Task] #34 installed and integrated tooBusy to send 503 when load is high * [Task] #34 improved tooBusy, improved formatting * [Task, Temp] #41 installed ratelimiter and slowDown * [Task] #42 cleanup ipv6 addresses * [Change] #10 error handling for better gitBash and txt output, also reduced stack in case of validation errors * [Task] #41 prepare Log for RateLImit errors * [Temp] #41 write route rateLImited temp: see Todos * [Task] #34 colorize prefix in console * [Task] #42 extract middlewares and move to folder * [Task] #41 ratelimiter cleaning up periodicly * [Task] #41 skip tests in rateLimiting --- .github/workflows/main.yml | 2 +- package-lock.json | 129 ++++++++++++++++++++++++++++++++-- package.json | 9 ++- src/app.ts | 103 +++++++++++++++++++++------ src/controller/read.ts | 2 +- src/controller/write.ts | 39 +++++----- src/{ => middleware}/cache.ts | 0 src/{ => middleware}/error.ts | 2 +- src/middleware/limit.ts | 59 ++++++++++++++++ src/models/entry.ts | 2 +- src/scripts/file.ts | 2 +- src/scripts/logger.ts | 44 +++++++++--- src/tests/integration.test.ts | 2 +- types.d.ts | 9 +++ 14 files changed, 347 insertions(+), 57 deletions(-) rename src/{ => middleware}/cache.ts (100%) rename src/{ => middleware}/error.ts (96%) create mode 100644 src/middleware/limit.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a755f3f..5eaaa85 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: - name: Start server run: | sudo npm start & - sleep 6 # Give server some time to start + sleep 8 # Give server some time to start - name: Check if server is running run: | curl --fail http://localhost:80 || exit 1 diff --git a/package-lock.json b/package-lock.json index 4c0b355..085305f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,21 +9,28 @@ "version": "0.0.1", "dependencies": { "chalk": "^4.1.2", + "compression": "^1.7.4", "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-slow-down": "^2.0.1", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", - "module-alias": "^2.2.3" + "module-alias": "^2.2.3", + "raw-body": "^2.5.2", + "toobusy-js": "^0.5.1" }, "devDependencies": { "@jest/globals": "^29.7.0", "@tsconfig/node20": "^20.1.2", "@types/bcrypt": "^5.0.2", + "@types/compression": "^1.7.5", "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", "@types/node": "^20.10.6", + "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", @@ -1513,6 +1520,15 @@ "@types/node": "*" } }, + "node_modules/@types/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -1680,6 +1696,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/toobusy-js": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@types/toobusy-js/-/toobusy-js-0.5.4.tgz", + "integrity": "sha512-hsKMbYiaL3ZWx7B3FYyN0rEJexw7I1HgKbNToX3ZZJv6373to954wlA7zrXR3/XoVwZnFwWqFguBs91sNzJGKQ==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -2306,6 +2328,20 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2595,6 +2631,47 @@ "node": ">= 0.8" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/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/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3348,6 +3425,34 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.5.tgz", + "integrity": "sha512-/iVogxu7ueadrepw1bS0X0kaRC/U0afwiYRSLg68Ts+p4Dc85Q5QKsOnPS/QUjPMHvOJQtBDrZgvkOzf8ejUYw==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "4 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express-slow-down": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/express-slow-down/-/express-slow-down-2.0.1.tgz", + "integrity": "sha512-zRogSZhNXJYKDBekhgFfFXGrOngH7Fub7Mx2g8OQ4RUBwSJP/3TVEKMgSGR/WlneT0mJ6NBUnidHhIELGVPe3w==", + "dependencies": { + "express-rate-limit": "7" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "express": ">= 4" + } + }, "node_modules/express-validator": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", @@ -5122,6 +5227,14 @@ "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==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5518,9 +5631,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -6031,6 +6144,14 @@ "node": ">=0.6" } }, + "node_modules/toobusy-js": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/toobusy-js/-/toobusy-js-0.5.1.tgz", + "integrity": "sha512-GiCux/c8G2TV0FTDgtxnXOxmSAndaI/9b1YxT14CqyeBDtTZAcJLx9KlXT3qECi8D0XCc78T4sN/7gWtjRyCaA==", + "engines": { + "node": ">=0.9.1" + } + }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", diff --git a/package.json b/package.json index 7e450e9..8b59750 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,13 @@ "@jest/globals": "^29.7.0", "@tsconfig/node20": "^20.1.2", "@types/bcrypt": "^5.0.2", + "@types/compression": "^1.7.5", "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", "@types/node": "^20.10.6", + "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", @@ -37,11 +39,16 @@ }, "dependencies": { "chalk": "^4.1.2", + "compression": "^1.7.4", "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-slow-down": "^2.0.1", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", - "module-alias": "^2.2.3" + "module-alias": "^2.2.3", + "raw-body": "^2.5.2", + "toobusy-js": "^0.5.1" }, "_moduleAliases": { "@src": "dist" diff --git a/src/app.ts b/src/app.ts index 0f5b50a..1e59d96 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,58 +1,117 @@ require('module-alias/register'); import { config } from 'dotenv'; import express from 'express'; +import toobusy from 'toobusy-js'; +// import { rateLimit } from 'express-rate-limit'; +// import { slowDown } from 'express-slow-down'; +import compression from 'compression'; import helmet from 'helmet'; import hpp from 'hpp'; -import cache from './cache'; -import * as error from "./error"; +import getRawBody from 'raw-body'; +import cache from './middleware/cache'; +import * as error from "./middleware/error"; import writeRouter from '@src/controller/write'; import readRouter from '@src/controller/read'; -import path from 'path'; +import path from 'path'; import logger from '@src/scripts/logger'; +// console.log({ "status": 403, "name": "Error", "message": { "errors": [{ "type": "field", "msg": "Invalid value", "path": "user", "location": "query" }, { "type": "field", "msg": "is required", "path": "lat", "location": "query" }]}}); +// console.log(JSON.stringify({ "status": 403, "name": "Error", "message": { "errors": [{ "type": "field", "msg": "Invalid value", "path": "user", "location": "query" }, { "type": "field", "msg": "is required", "path": "lat", "location": "query" }]}}, null, 2)); + // configurations -config(); +config(); // dotenv + const app = express(); -app.use( - helmet({ - contentSecurityPolicy: { - directives: { - "default-src": "'self'", - "img-src": "*" - } - } - }) -); -app.use(hpp()); +app.use((req, res, next) => { // monitor eventloop to block requests if busy + if (toobusy()) { + res.status(503).set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Retry-After': '60' }).send("I'm busy right now, sorry."); + } else { next(); } +}); +app.use((req, res, next) => { // clean up IPv6 Addresses + if (req.ip) { + res.locals.ip = req.ip.startsWith('::ffff:') ? req.ip.substring(7) : req.ip; + next(); + } else { + const message = "No IP provided" + logger.error(message); + res.status(400).send(message); + } + +}) + +// const slowDownLimiter = slowDown({ +// windowMs: 1 * 60 * 1000, +// delayAfter: 5, // Allow 5 requests per 15 minutes. +// delayMs: (used) => (used - 5) * 1000, // Add delay after delayAfter is reached +// }) + +// const rateLimiter = rateLimit({ +// windowMs: 1 * 60 * 1000, +// limit: 10, // Limit each IP per `window` +// standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers +// legacyHeaders: false, // Disable the `X-RateLimit-*` headers +// }) + +app.use(helmet({ contentSecurityPolicy: { directives: { "default-src": "'self'", "img-src": "*" } } })); app.use(cache); +app.use(compression()) +app.use(hpp()); +app.use(function (req, res, next) { // limit request size limit when recieving data + if (!['POST', 'PUT', 'DELETE'].includes(req.method)) { return next(); } + getRawBody(req, { length: req.headers['content-length'], limit: '1mb', encoding: true }, + function (err) { + if (err) { return next(err) } + next() + } + ) +}) // routes app.get('/', (req, res) => { - res.send('Hello World, via TypeScript and Node.js!'); + console.log(req.ip + " - " + res.locals.ip); + res.send('Hello World, via TypeScript and Node.js! ' + res.locals.ip); }); - app.use('/write', writeRouter); app.use('/read', readRouter); // use httpdocs as static folder app.use('/', express.static(path.join(__dirname, 'httpdocs'), { extensions: ['html', 'txt', "pdf"], - index: "start.html", -})) + index: ["start.html", "start.txt"], +})); // error handling app.use(error.notFound); app.use(error.handler); // init server -app.listen(80, () => { - logger.log(`Server running //localhost:80, ENV: ${process.env.NODE_ENV}`, true); +const server = app.listen(80, () => { + logger.log(`Server running //localhost:80, ENV: ${process.env.NODE_ENV}`, true); +}); + +// catching shutdowns +['SIGINT', 'SIGTERM', 'exit'].forEach((signal) => { + process.on(signal, () => { + function logAndExit() { + // calling .shutdown allows your process to exit normally + toobusy.shutdown(); + logger.log(`Server shutdown on signal: ${signal} //localhost:80`, true); + process.exit(); + } + if (signal != "exit") { // give the server time to shutdown before closing + server.close(logAndExit); + } else { + logger.log(`Server shutdown immediate: ${signal} //localhost:80`, true); + } + }); }); -process.on('uncaughtException', function(err) { +// last resort error handling +process.on('uncaughtException', function (err) { console.error('Caught exception:', err); logger.error(err); + server.close(); process.exit(1); }); \ No newline at end of file diff --git a/src/controller/read.ts b/src/controller/read.ts index f17c93c..02294dc 100644 --- a/src/controller/read.ts +++ b/src/controller/read.ts @@ -1,6 +1,6 @@ import express, { Request, Response, NextFunction } from 'express'; import * as file from '@src/scripts/file'; -import { create as createError } from '@src/error'; +import { create as createError } from '@src/middleware/error'; import { validationResult, query } from 'express-validator'; const router = express.Router(); diff --git a/src/controller/write.ts b/src/controller/write.ts index dc0658d..7b405ff 100644 --- a/src/controller/write.ts +++ b/src/controller/write.ts @@ -1,19 +1,25 @@ import express, { Request, Response, NextFunction } from 'express'; import { entry } from '@src/models/entry'; import { validationResult } from 'express-validator'; -import { create as createError } from '@src/error'; - +import { create as createError } from '@src/middleware/error'; +import { baseSlowDown, errorRateLimiter } from '@src/middleware/limit'; // example call: /write?user=xx&lat=00.000&lon=00.000×tamp=1704063600000&hdop=0.0&altitude=0.000&speed=0.000&heading=000.0 -async function errorChecking (req:Request, res:Response, next:NextFunction) { + +function errorChecking(req: Request, res: Response, next: NextFunction) { const errors = validationResult(req); if (!errors.isEmpty()) { - const errorAsJson = { errors: errors.array()}; - const errorAsString = JSON.stringify(errorAsJson); - const hasKeyErrors = errors.array().some(error => error.msg.includes("Key")); - - // send forbidden or unprocessable content - return createError(res, hasKeyErrors ? 403 : 422, errorAsString, next) + // if errors happend, then rateLimit to prevent key bruteforcing + errorRateLimiter(req, res, () => { + const errorAsJson = { errors: errors.array() }; + const errorAsString = JSON.stringify(errorAsJson); + const hasKeyErrors = errors.array().some(error => error.msg.includes("Key")); + + // send forbidden or unprocessable content + return createError(res, hasKeyErrors ? 403 : 422, errorAsString, next) + }); + + return; } if (req.method == "HEAD") { @@ -21,22 +27,23 @@ async function errorChecking (req:Request, res:Response, next:NextFunction) { return; } + next(); +} + +async function writeData(req: Request, res: Response, next: NextFunction) { // Regular Save logic from here await entry.create(req, res, next); - if (!res.locals.error) { + if (!res.locals.error) { res.send(req.query); - } else { - /* at this point error handling already happend, - * or the request has already been send - * therefor there is no need for it again (only middleware to follow at this point) */ + } else { next(); } } const router = express.Router(); -router.get('/', entry.validate, errorChecking); -router.head('/', entry.validate, errorChecking); + router.get('/', baseSlowDown, entry.validate, errorChecking, writeData); +router.head('/', baseSlowDown, entry.validate, errorChecking); export default router; \ No newline at end of file diff --git a/src/cache.ts b/src/middleware/cache.ts similarity index 100% rename from src/cache.ts rename to src/middleware/cache.ts diff --git a/src/error.ts b/src/middleware/error.ts similarity index 96% rename from src/error.ts rename to src/middleware/error.ts index 7b8d624..2eaf6e8 100644 --- a/src/error.ts +++ b/src/middleware/error.ts @@ -29,7 +29,7 @@ export function handler(err: Error, req: Request, res: Response message = err.message; } - const responseBody = { + const responseBody:Response.Error = { status: statusCode, name: err.name, message: message, diff --git a/src/middleware/limit.ts b/src/middleware/limit.ts new file mode 100644 index 0000000..a73e6c8 --- /dev/null +++ b/src/middleware/limit.ts @@ -0,0 +1,59 @@ +import { Request, Response, NextFunction } from 'express'; +import { rateLimit, Options as rateLimiterOptions } from 'express-rate-limit'; +import { slowDown, Options as slowDownOptions } from 'express-slow-down'; +import logger from '@src/scripts/logger'; + + +/* +** configurations +*/ + +const baseOptions: Partial = { + windowMs: 30 * 60 * 1000, + skip: (req, res) => (res.locals.ip == "127.0.0.1" || res.locals.ip == "::1") +} + +const baseSlowDownOptions: Partial = { + ...baseOptions, + delayAfter: 3, // no delay for amount of attempts + delayMs: (used: number) => (used - 3) * 125, // Add delay after delayAfter is reached +} + +const baseRateLimitOptions: Partial = { + ...baseOptions, + limit: 10, // Limit each IP per window + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers +} + + +/* +** cleanup +*/ +const ipsThatReachedLimit: RateLimit.obj = {}; // prevent logs from flooding +setInterval(() => { + const oneHourAgo = Date.now() - 60 * 60 * 1000; + for (const ip in ipsThatReachedLimit) { + if (ipsThatReachedLimit[ip].time < oneHourAgo) { + delete ipsThatReachedLimit[ip]; + } + } +}, 60 * 60 * 1000); + + +/* +** exported section +*/ +export const baseSlowDown = slowDown(baseSlowDownOptions); + +export const errorRateLimiter = rateLimit({ + ...baseRateLimitOptions, + message: 'Too many requests with errors', + handler: (req: Request, res: Response, next: NextFunction, options: rateLimiterOptions) => { + if (!Object.prototype.hasOwnProperty.call(ipsThatReachedLimit, res.locals.ip)) { + logger.error(`[RateLimit] for invalid requests reached ${res.locals.ip}, ${req.get('User-Agent')}`); + ipsThatReachedLimit[res.locals.ip] = { limitReachedOnError: true, time: Date.now() }; + } + res.status(options.statusCode).send(options.message); + } +}); \ No newline at end of file diff --git a/src/models/entry.ts b/src/models/entry.ts index 4305fea..1f510bf 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, Response } from 'express'; import { checkExact, query } from 'express-validator'; import { crypt } from '@src/scripts/crypt'; -import { create as createError } from '@src/error'; +import { create as createError } from '@src/middleware/error'; import * as file from '@src/scripts/file'; import { getTime } from '@src/scripts/time'; import { getSpeed } from '@src/scripts/speed'; diff --git a/src/scripts/file.ts b/src/scripts/file.ts index 5fdec3f..a8b693b 100644 --- a/src/scripts/file.ts +++ b/src/scripts/file.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; import { promisify } from 'util'; -import { create as createError } from '@src/error'; +import { create as createError } from '@src/middleware/error'; import { NextFunction, Response } from 'express'; import logger from '@src/scripts/logger'; diff --git a/src/scripts/logger.ts b/src/scripts/logger.ts index 0beeaea..67effdd 100644 --- a/src/scripts/logger.ts +++ b/src/scripts/logger.ts @@ -1,13 +1,20 @@ // primitive text logger -import fs from 'fs'; // typescript will compile to require -import path from 'path'; // typescript will compile to require -import chalk from "chalk"; // keep import syntax after compile +import fs from 'fs'; +import path from 'path'; +import chalk from "chalk"; -const logPath = path.resolve(__dirname, '../httpdocs', 'log.txt'); +const dirPath = path.resolve(__dirname, '../httpdocs/log'); +const logPath = path.resolve(dirPath, 'start.txt'); + +if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); +} + +// const logPath = path.resolve(__dirname, '../httpdocs/log', 'start.txt'); const date = new Date().toLocaleString('de-DE', { hour12: false }); export default { - log: (message:string|JSON, showDateInConsole:boolean=false, showLogInTest=false) => { + log: (message: string | JSON, showDateInConsole: boolean = false, showLogInTest = false) => { message = JSON.stringify(message); fs.appendFileSync(logPath, `${date} \t|\t ${message} \n`); if (showDateInConsole) { @@ -17,8 +24,29 @@ export default { console.log(message); } }, - error: (message:string|JSON|Response.Error) => { - fs.appendFileSync(logPath, `${date} \t|\t ERROR: ${message} \n`); - console.error(message); + error: (content: string | Response.Error) => { + // logfile + const applyErrorPrefix = !/^\[\w+\]/.test(typeof content == "string" ? content : content.message); + const logMessageTemplate = `${date} \t|\t${applyErrorPrefix ? ' [ERROR]' : ''} ${typeof content == "string" ? content : JSON.stringify(content.message) } \n`; + fs.appendFileSync(logPath, logMessageTemplate); + if (process.env.NODE_ENV == "production") { return; } + + // console + if (typeof content != "string" && Object.hasOwnProperty.call(content, "message")) { + const messageAsString = JSON.stringify(content.message); + if (content.stack) { // replace redundant information + content.stack = content.stack.replace(messageAsString, ""); + } + const consoleMessage = structuredClone(content); // create clone so response output is not "further" affected + consoleMessage.message = messageAsString; // gitbash output improvement (w/o objects in arrays appear as [Object]) + content = consoleMessage; + } else if (typeof content == "string") { + const prefix = content.match(/^\[\w+\]/); + if (prefix?.length) { + content = content.replace(prefix[0], chalk.red(prefix[0])); + } + } + console.error(content); // log string right away or processed Object + } } diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index 2d40ced..6133c1d 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -129,7 +129,7 @@ describe("GET /write", () => { expect(entry.time.created).toBeGreaterThan(date.getTime()); expect(entry.time.diff).toBeGreaterThan(3.5); - expect(entry.time.diff).toBeLessThan(4); + expect(entry.time.diff).toBeLessThan(4.6); const germanDayPattern = "(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)"; diff --git a/types.d.ts b/types.d.ts index 7cb4c68..8931ce5 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,6 +1,15 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +namespace RateLimit { + interface obj { + [key: string]: { + limitReachedOnError: boolean, + time: number + } + } +} + namespace Response { interface Message { message: string; From ce6e636c017d412c2535bcf228642f8ca5bb76ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:36:19 +0100 Subject: [PATCH 055/206] Bump follow-redirects from 1.15.5 to 1.15.6 (#47) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 085305f..9b1e43b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3595,9 +3595,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { From 45c3a898a4586086866817409f4143666c6ff9d7 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 21 Mar 2024 14:14:44 +0100 Subject: [PATCH 056/206] 43 secure output route (#46) * [Task] #43 create color pallette via atmos * [Task] #43 create color pallette via atmos * [Task] #43 cleanup colors and svg * [Task] #41 remove test code * [CHANGE] #3 reconfigured nodemon to copy static files * [Task] #18 replaced getRawBody with builtIn express urlEncoded * [Temp, Task] #43 basic login page, not yet used as middleware * [Temp] #43, create and validate json web token * [Task] #43, add slowDown and RateLimit for failed login attempts * [Task] #43, ratelimit for login page * [Task] #43, add global ratelimiter * [fix] #7, improve error handeling for express errors * [Task] #43 rework body limitations to be checked only appropiate methods * [Task] #43 added check for data before using it * [Task] #43 check that body is ignored for GET in request * [Task] #43 login test * [Task] #43 create tests for login * [Task] #43 fine tune error handling * [Task] #43, finished login and jwt related tests * [Change] #34, no further need for test logging * [Task] #43, fine tune jwt, middleware process improved * [CHANGE] #43 created new esLint to have clientside js without ts * [Temp] #43 test to see new linter configuration * [Change] #43 switched to bcrypt for passwords * [Task] #43 read return json in all cases * [Task] #43 introduced color classes * [Task] #43, prq feedback * [Temp} #43 figuring out why tests dont run on github * [Task] #43 code cleanup --- .eslintrc.json | 2 +- .github/workflows/eslint.yml | 9 +- .github/workflows/main.yml | 28 +- httpdocs/color-table.svg | 159 +++++ httpdocs/css/colors.css | 95 +++ httpdocs/css/login.css | 20 + httpdocs/js/.eslintrc.json | 12 + httpdocs/js/login.js | 1 + nodemon-static.json | 8 + nodemon.json => nodemon-ts.json | 0 package-lock.json | 833 +++++++++++++++++++--- package.json | 23 +- src/app.ts | 46 +- src/controller/read.ts | 139 +++- src/controller/write.ts | 2 +- src/middleware/error.ts | 7 +- src/middleware/limit.ts | 35 +- src/models/entry.ts | 25 +- src/scripts/crypt.ts | 25 +- src/scripts/logger.ts | 7 +- src/tests/app.test.ts | 29 +- src/tests/integration.test.ts | 86 ++- src/tests/login.test.ts | 58 ++ src/tests/{entry.test.ts => unit.test.ts} | 4 +- types.d.ts | 6 +- views/login-form.ejs | 30 + 26 files changed, 1485 insertions(+), 204 deletions(-) create mode 100644 httpdocs/color-table.svg create mode 100644 httpdocs/css/colors.css create mode 100644 httpdocs/css/login.css create mode 100644 httpdocs/js/.eslintrc.json create mode 100644 httpdocs/js/login.js create mode 100644 nodemon-static.json rename nodemon.json => nodemon-ts.json (100%) create mode 100644 src/tests/login.test.ts rename src/tests/{entry.test.ts => unit.test.ts} (96%) create mode 100644 views/login-form.ejs diff --git a/.eslintrc.json b/.eslintrc.json index 30be6b6..aa4c83b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,6 +22,6 @@ //"@typescript-eslint/no-unused-vars": "warn" "jest/no-conditional-expect": "off" }, - "ignorePatterns": ["dist", "jest.config.js"] + "ignorePatterns": ["dist", "jest.config.js", "httpdocs"] } diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 3aed66a..95578d7 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -15,8 +15,7 @@ jobs: with: node-version: 16 - run: npm ci # or yarn install - - uses: sibiraj-s/action-eslint@v3 - with: - eslint-args: '--ignore-path=.gitignore --quiet' - extensions: 'js,jsx,ts,tsx' - annotations: true + - name: Lint server-side code + run: npx eslint src/ --fix + - name: Lint client-side code + run: npx eslint httpdocs/js/ --fix diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5eaaa85..e31f256 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Node3 +name: Tests on: workflow_dispatch: @@ -10,7 +10,13 @@ on: jobs: build: runs-on: ubuntu-latest - + env: + NODE_ENV: ${{ vars.NODE_ENV }} + LOCALHOST: ${{ vars.LOCALHOST }} + LOCALHOSTV6: ${{ vars.LOCALHOSTV6 }} + KEYA: ${{ secrets.KEYA }} + KEYB: ${{ secrets.KEYB }} + USER_TEST: ${{ secrets.USER_TEST }} steps: - uses: actions/checkout@v3 @@ -19,14 +25,24 @@ jobs: with: node-version: '20' cache: 'npm' + - run: echo "NODE_ENV = $NODE_ENV" - run: npm ci - run: npm run build --if-present - name: Start server run: | - sudo npm start & - sleep 8 # Give server some time to start + sudo NODE_ENV=$NODE_ENV LOCALHOST=$LOCALHOST LOCALHOSTV6=$LOCALHOSTV6 KEYA=$KEYA KEYB=$KEYB USER_TEST=$USER_TEST npm start & + sleep 15 # Give server some time to start - name: Check if server is running run: | curl --fail http://localhost:80 || exit 1 - - name: Run tests - run: npm run test \ No newline at end of file + - name: Run app tests + run: npm run test:app + - name: Run login tests + run: npm run test:login + - name: Run unit tests + run: npm run test:unit + - name: Run integration tests + run: npm run test:integration + + + \ No newline at end of file diff --git a/httpdocs/color-table.svg b/httpdocs/color-table.svg new file mode 100644 index 0000000..d299941 --- /dev/null +++ b/httpdocs/color-table.svg @@ -0,0 +1,159 @@ + + + + + + HEX + + + main + + + info + + + alert + + + success + + + neutral + + + + OKLCH + + + + + 900 + + + + 750 + + + + 625 + + + 500 + + + 375 + + + 250 + + + 100 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/httpdocs/css/colors.css b/httpdocs/css/colors.css new file mode 100644 index 0000000..d77a7fd --- /dev/null +++ b/httpdocs/css/colors.css @@ -0,0 +1,95 @@ +/* +created by atmos https://app.atmos.style/65cc9eaec76d443c0a796d4b + +** base configuration colors ** +Main: #f90 +Info: #231aee +Danger: #ff0000 +Success: #59ec04 +Neutral: #131211 +*/ + +[class*=color] { + --lightness: 67.66%; + --hue: 64.55; + --chroma: 0.007; + color: oklch(var(--lightness) var(--chroma) var(--hue)); + + &[class*=l1] {--lightness: 10%;} + &[class*=l2] {--lightness: 25%;} + &[class*=l3] {--lightness: 37.5%;} + &[class*=l4] {--lightness: 50%;} + &[class*=l5] {--lightness: 62.5%;} + &[class*=l6] {--lightness: 77.2%;} + &[class*=l7] {--lightness: 90%;} + + &[class*=main] { + --lightness: 77.2%; + --chroma: 0.1738; + --hue: 64.55; + + &[class*=l1] {--chroma: 0.02;} + &[class*=l2] {--chroma: 0.056;} + &[class*=l3] {--chroma: 0.085;} + &[class*=l4] {--chroma: 0.114;} + &[class*=l5] {--chroma: 0.142;} + &[class*=l6] {--chroma: 0.1738;} /* base */ + &[class*=l7] {--chroma: 0.06;} + } + + &[class*=info] { + --lightness: 44.87%; + --chroma: 0.2838; + --hue: 268.0; + + &[class*=l1] {--chroma: 0.055;} + &[class*=l2] {--chroma: 0.158;} + &[class*=l3] {--chroma: 0.237;} + &[class*=l4] {--chroma: 0.2838;} /* base */ + &[class*=l5] {--chroma: 0.19;} + &[class*=l6] {--chroma: 0.109;} + &[class*=l7] {--chroma: 0.04;} + } + + &[class*=alert] { + --lightness: 62.8%; + --chroma: 0.2577; + --hue: 29.23; + + &[class*=l1] {--chroma: 0.036;} + &[class*=l2] {--chroma: 0.103;} + &[class*=l3] {--chroma: 0.154;} + &[class*=l4] {--chroma: 0.195;} + &[class*=l5] {--chroma: 0.2577;} /* base */ + &[class*=l6] {--chroma: 0.133;} + &[class*=l7] {--chroma: 0.045;} + } + + &[class*=success] { + --lightness: 83%; + --chroma: 0.2607; + --hue: 138.96; + + &[class*=l1] {--chroma: 0.029;} + &[class*=l2] {--chroma: 0.083;} + &[class*=l3] {--chroma: 0.124;} + &[class*=l4] {--chroma: 0.157;} + &[class*=l5] {--chroma: 0.208;} + &[class*=l6] {--chroma: 0.2607;} /* base */ + &[class*=l7] {--chroma: 0.201;} + } + + &[class*=neutral] { + --lightness: 18.3%; + --chroma: 0.0026; + --hue: 67.66; + + &[class*=l1] {--chroma: 0.001;} + &[class*=l2] {--chroma: 0.0026;} /* base */ + &[class*=l3] {--chroma: 0.006;} + &[class*=l4] {--chroma: 0.007;} + &[class*=l5] {--chroma: 0.009;} + &[class*=l6] {--chroma: 0.011;} + &[class*=l7] {--chroma: 0.004;} + } +} diff --git a/httpdocs/css/login.css b/httpdocs/css/login.css new file mode 100644 index 0000000..b9d1d7c --- /dev/null +++ b/httpdocs/css/login.css @@ -0,0 +1,20 @@ +form { + margin-inline: auto; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + max-width: 500px; + gap: 10px; +} +input, button { + flex-grow: 1; +} +textarea, h1 { + flex-basis: 100%; +} +textarea { + height: 50vh; +} +h1 { + text-align: center; +} diff --git a/httpdocs/js/.eslintrc.json b/httpdocs/js/.eslintrc.json new file mode 100644 index 0000000..487f0f6 --- /dev/null +++ b/httpdocs/js/.eslintrc.json @@ -0,0 +1,12 @@ +{ + "root": true, + "env": { + "browser": true, + "es2021": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2021, + "sourceType": "module" + } +} diff --git a/httpdocs/js/login.js b/httpdocs/js/login.js new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/httpdocs/js/login.js @@ -0,0 +1 @@ + diff --git a/nodemon-static.json b/nodemon-static.json new file mode 100644 index 0000000..221cf6c --- /dev/null +++ b/nodemon-static.json @@ -0,0 +1,8 @@ +{ + "watch": [ + "httpdocs" + ], + "ext": "*", + "ignore": [], + "exec": "cp -R httpdocs/ dist/" +} \ No newline at end of file diff --git a/nodemon.json b/nodemon-ts.json similarity index 100% rename from nodemon.json rename to nodemon-ts.json diff --git a/package-lock.json b/package-lock.json index 9b1e43b..bae40f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,18 @@ "name": "lorex", "version": "0.0.1", "dependencies": { + "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", + "ejs": "^3.1.9", "express": "^4.18.2", "express-rate-limit": "^7.1.5", "express-slow-down": "^2.0.1", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", + "jsonwebtoken": "^9.0.2", "module-alias": "^2.2.3", - "raw-body": "^2.5.2", "toobusy-js": "^0.5.1" }, "devDependencies": { @@ -25,15 +27,16 @@ "@tsconfig/node20": "^20.1.2", "@types/bcrypt": "^5.0.2", "@types/compression": "^1.7.5", - "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", + "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.10.6", "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", + "concurrently": "^8.2.2", "dotenv": "^16.3.1", "eslint": "^8.56.0", "eslint-plugin-jest": "^27.6.3", @@ -657,6 +660,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -1371,6 +1386,61 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1538,16 +1608,6 @@ "@types/node": "*" } }, - "node_modules/@types/dotenv": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw==", - "deprecated": "This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "dotenv": "*" - } - }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -1636,6 +1696,15 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "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, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2032,8 +2101,7 @@ "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==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/accepts": { "version": "1.3.8", @@ -2077,6 +2145,38 @@ "node": ">=0.4.0" } }, + "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==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/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/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2124,7 +2224,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2156,6 +2255,23 @@ "node": ">= 8" } }, + "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==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -2182,6 +2298,11 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2293,8 +2414,20 @@ "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==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -2346,7 +2479,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2417,6 +2549,11 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2432,13 +2569,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "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.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2552,6 +2694,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -2619,6 +2769,14 @@ "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==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2675,8 +2833,63 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==" }, "node_modules/content-disposition": { "version": "0.5.4", @@ -2757,6 +2970,22 @@ "node": ">= 8" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2795,16 +3024,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "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": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "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/delayed-stream": { @@ -2816,6 +3048,11 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2833,6 +3070,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2896,11 +3141,33 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.640", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.640.tgz", @@ -2922,8 +3189,7 @@ "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==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -2942,6 +3208,25 @@ "is-arrayish": "^0.2.1" } }, + "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.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3529,6 +3814,33 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/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/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3644,11 +3956,32 @@ "node": ">= 0.6" } }, + "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==", + "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==", + "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==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -3672,6 +4005,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3691,15 +4043,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "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" } @@ -3729,7 +4085,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3825,11 +4180,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "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": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3857,10 +4212,15 @@ "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==" + }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", "dependencies": { "function-bind": "^1.1.2" }, @@ -3909,6 +4269,39 @@ "node": ">= 0.8" } }, + "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==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/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/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3992,7 +4385,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -4054,7 +4446,6 @@ "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==", - "dev": true, "engines": { "node": ">=8" } @@ -4235,6 +4626,23 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -4865,6 +5273,51 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "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==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4931,6 +5384,36 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -4943,11 +5426,15 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -5077,7 +5564,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5085,6 +5571,48 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "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==", + "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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/module-alias": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", @@ -5109,6 +5637,30 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5208,6 +5760,25 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.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.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -5239,7 +5810,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -5366,7 +5936,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5630,26 +6199,25 @@ "node": ">= 0.6" } }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "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/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5662,6 +6230,12 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5741,7 +6315,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -5775,6 +6348,21 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5803,7 +6391,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -5856,15 +6443,22 @@ "node": ">= 0.8.0" } }, + "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.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -5896,6 +6490,15 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -5912,8 +6515,7 @@ "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==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-update-notifier": { "version": "2.0.0", @@ -5961,6 +6563,12 @@ "source-map": "^0.6.0" } }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -5996,6 +6604,14 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6013,7 +6629,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -6027,7 +6642,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6089,6 +6703,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "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/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6164,6 +6794,20 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -6400,6 +7044,11 @@ "punycode": "^2.1.0" } }, + "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", @@ -6463,6 +7112,20 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6478,6 +7141,14 @@ "node": ">= 8" } }, + "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==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -6498,8 +7169,7 @@ "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==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.2", @@ -6526,8 +7196,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index 8b59750..d42439d 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,20 @@ "main": "index.js", "scripts": { "prebuild": "rm -rf dist/*", - "build": "npx tsc && cp -R httpdocs/ dist/", - "build:prod": "npx tsc -p ./tsconfig.prod.json && cp -R httpdocs/ dist/", + "build": "npx tsc", + "build:prod": "npx tsc -p ./tsconfig.prod.json", + "postbuild": "cp -R httpdocs/ dist/", "start": "node dist/app.js", - "dev": "rm -rf dist/* && cp -R httpdocs/ dist/ && nodemon src/app.ts", + "dev": "npm run prebuild && npm run postbuild && concurrently \"npm:dev:*\"", + "dev:ts": "nodemon --config nodemon-ts.json", + "dev:static": "nodemon --config nodemon-static.json", "lint": "eslint . --fix", - "test": "jest" + "lint:client": "eslint httpdocs/js/ --fix", + "test": "jest", + "test:app": "jest src/tests/app.test.ts", + "test:login": "jest src/tests/login.test.ts", + "test:unit": "jest src/tests/unit.test.ts", + "test:integration": "jest src/tests/integration.test.ts" }, "keywords": [], "author": "Type-Style", @@ -19,15 +27,16 @@ "@tsconfig/node20": "^20.1.2", "@types/bcrypt": "^5.0.2", "@types/compression": "^1.7.5", - "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", + "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.10.6", "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", + "concurrently": "^8.2.2", "dotenv": "^16.3.1", "eslint": "^8.56.0", "eslint-plugin-jest": "^27.6.3", @@ -38,16 +47,18 @@ "typescript": "^5.3.3" }, "dependencies": { + "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", + "ejs": "^3.1.9", "express": "^4.18.2", "express-rate-limit": "^7.1.5", "express-slow-down": "^2.0.1", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", + "jsonwebtoken": "^9.0.2", "module-alias": "^2.2.3", - "raw-body": "^2.5.2", "toobusy-js": "^0.5.1" }, "_moduleAliases": { diff --git a/src/app.ts b/src/app.ts index 1e59d96..ce1edff 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,27 +2,24 @@ require('module-alias/register'); import { config } from 'dotenv'; import express from 'express'; import toobusy from 'toobusy-js'; -// import { rateLimit } from 'express-rate-limit'; -// import { slowDown } from 'express-slow-down'; import compression from 'compression'; import helmet from 'helmet'; import hpp from 'hpp'; -import getRawBody from 'raw-body'; import cache from './middleware/cache'; import * as error from "./middleware/error"; import writeRouter from '@src/controller/write'; import readRouter from '@src/controller/read'; import path from 'path'; import logger from '@src/scripts/logger'; - -// console.log({ "status": 403, "name": "Error", "message": { "errors": [{ "type": "field", "msg": "Invalid value", "path": "user", "location": "query" }, { "type": "field", "msg": "is required", "path": "lat", "location": "query" }]}}); -// console.log(JSON.stringify({ "status": 403, "name": "Error", "message": { "errors": [{ "type": "field", "msg": "Invalid value", "path": "user", "location": "query" }, { "type": "field", "msg": "is required", "path": "lat", "location": "query" }]}}, null, 2)); +import { baseRateLimiter } from './middleware/limit'; // configurations config(); // dotenv const app = express(); +app.set('view engine', 'ejs'); + app.use((req, res, next) => { // monitor eventloop to block requests if busy if (toobusy()) { res.status(503).set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Retry-After': '60' }).send("I'm busy right now, sorry."); @@ -33,44 +30,29 @@ app.use((req, res, next) => { // clean up IPv6 Addresses res.locals.ip = req.ip.startsWith('::ffff:') ? req.ip.substring(7) : req.ip; next(); } else { - const message = "No IP provided" + const message = "No IP provided"; logger.error(message); res.status(400).send(message); } - }) -// const slowDownLimiter = slowDown({ -// windowMs: 1 * 60 * 1000, -// delayAfter: 5, // Allow 5 requests per 15 minutes. -// delayMs: (used) => (used - 5) * 1000, // Add delay after delayAfter is reached -// }) - -// const rateLimiter = rateLimit({ -// windowMs: 1 * 60 * 1000, -// limit: 10, // Limit each IP per `window` -// standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers -// legacyHeaders: false, // Disable the `X-RateLimit-*` headers -// }) - app.use(helmet({ contentSecurityPolicy: { directives: { "default-src": "'self'", "img-src": "*" } } })); app.use(cache); app.use(compression()) app.use(hpp()); -app.use(function (req, res, next) { // limit request size limit when recieving data - if (!['POST', 'PUT', 'DELETE'].includes(req.method)) { return next(); } - getRawBody(req, { length: req.headers['content-length'], limit: '1mb', encoding: true }, - function (err) { - if (err) { return next(err) } - next() - } - ) -}) +app.use(baseRateLimiter); +app.use((req, res, next) => { // limit body for specific http methods + if(['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) { + return express.urlencoded({ limit: '0.5kb', extended: true })(req, res, next); + } + next(); +}); + // routes app.get('/', (req, res) => { - console.log(req.ip + " - " + res.locals.ip); - res.send('Hello World, via TypeScript and Node.js! ' + res.locals.ip); + logger.log(req.ip + " - " + res.locals.ip, true); + res.send('Hello World, via TypeScript and Node.js! ' + `ENV: ${process.env.NODE_ENV}`); }); app.use('/write', writeRouter); diff --git a/src/controller/read.ts b/src/controller/read.ts index 02294dc..b04893d 100644 --- a/src/controller/read.ts +++ b/src/controller/read.ts @@ -2,33 +2,138 @@ import express, { Request, Response, NextFunction } from 'express'; import * as file from '@src/scripts/file'; import { create as createError } from '@src/middleware/error'; import { validationResult, query } from 'express-validator'; +import jwt from 'jsonwebtoken'; +import logger from '@src/scripts/logger'; +import { crypt, compare } from '@src/scripts/crypt'; +import { loginSlowDown, loginLimiter, baseSlowDown, baseRateLimiter } from '@src/middleware/limit'; const router = express.Router(); -router.get('/', - [query('index').isInt().withMessage("not an integer") +router.get('/', + isLoggedIn, + [query('index').isInt().withMessage("not an integer") .isLength({ max: 3 }).withMessage("not in range") .toInt()], - async function getRead(req:Request, res:Response, next:NextFunction) { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return createError(res, 400, JSON.stringify({errors: errors.array()}), next) - } + async function getRead(req: Request, res: Response, next: NextFunction) { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return createError(res, 400, JSON.stringify({ errors: errors.array() }), next) + } + + const fileObj: File.Obj = file.getFile(res, next); + fileObj.content = await file.readAsJson(res, fileObj.path, next) + if (!fileObj.content || !Array.isArray(fileObj.content.entries)) { + return createError(res, undefined, `File corrupt: ${fileObj.path}`, next); + } + + let entries = fileObj.content.entries; + + if (req.query.index) { + entries = entries.slice(Number(req.query.index)); + } + + res.json({ entries }); + }); + +router.get("/login/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response) { + res.locals.text = "start"; + loginLimiter(req, res, () => { + res.render("login-form"); + }); +}); + +router.post("/login/", loginSlowDown, async function postLogin(req: Request, res: Response, next: NextFunction) { + logger.log(req.body); + loginLimiter(req, res, async () => { + let validLogin = false; + const user = req.body.user; + const password = req.body.password; + let userFound = false; + if (!user || !password) { + return createError(res, 422, "Body does not contain all expected information", next); + } + + // Loop through all environment variables + for (const key in process.env) { + if (!key.startsWith('USER')) { continue; } + if (key.substring(5) == user) { + userFound = true; + const hash = process.env[key]; + if (hash) { + validLogin = await compare(password, hash); + } + } + } - const fileObj: File.Obj = file.getFile(res, next); - fileObj.content = await file.readAsJson(res, fileObj.path, next) - if (!fileObj.content || !Array.isArray(fileObj.content.entries)) { - return createError(res, undefined, `File corrupt: ${fileObj.path}`, next); + // only allow test user in test environment + if (user == "TEST" && validLogin && process.env.NODE_ENV == "production") { + validLogin = false; + } + + if (validLogin) { + const token = createToken(req, res); + res.json({ "token": token }); + } else { + if (!userFound) { + await crypt(password); // If no matching user is found, perform a dummy password comparison to prevent timing attacks + } + return createError(res, 403, `invalid login credentials`, next); + } + }); +}); + +function isLoggedIn(req: Request, res: Response, next: NextFunction) { + const result = validateToken(req); + if (!result.success) { + createError(res, result.status, result.message || "", next) + } else { + next(); } +} + +function validateToken(req: Request) { + const key = process.env.KEYA; + const header = req.header('Authorization'); + const [type, token] = header ? header.split(' ') : ""; + let payload: string | jwt.JwtPayload = ""; - let entries = fileObj.content.entries; + // Guard; aka early return for common failures before verifying authorization + if (!key) { return { success: false, status: 500, message: 'Wrong Configuration' }; } + if (!header) { return { success: false, status: 401, message: 'No Authorization header' }; } + if (type !== 'Bearer' || !token) { return { success: false, status: 400, message: 'Invalid Authorization header' }; } - if (req.query.index) { - entries = entries.slice(Number(req.query.index)); + try { + payload = jwt.verify(token, key); + } catch (err) { + let message = "could not verify"; + if (err instanceof Error) { + message = `${err.name} - ${err.message}`; + } + + return { success: false, status: 403, message: message }; } - res.json({entries}); -}); + // don't allow test user in production environment + if (typeof payload == "object" && payload.user == "TEST" && process.env.NODE_ENV == "production") { + return { success: false, status: 403, message: 'test user not allowed on production' }; + } + + return { success: true }; +} +function createToken(req: Request, res: Response) { + const key = process.env.KEYA; + if (!key) { throw new Error('Configuration is wrong'); } + const today = new Date(); + const dateString = today.toLocaleDateString("de-DE", { weekday: "short", year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); + const payload = { + date: dateString, + user: req.body.user + }; + const token = jwt.sign(payload, key, { expiresIn: 60 * 2 }); + res.locals.token = token; + logger.log(JSON.stringify(payload), true); + return token; +} -export default router; \ No newline at end of file +export default router; diff --git a/src/controller/write.ts b/src/controller/write.ts index 7b405ff..28aa439 100644 --- a/src/controller/write.ts +++ b/src/controller/write.ts @@ -43,7 +43,7 @@ async function writeData(req: Request, res: Response, next: NextFunction) { const router = express.Router(); - router.get('/', baseSlowDown, entry.validate, errorChecking, writeData); +router.get('/', baseSlowDown, entry.validate, errorChecking, writeData); router.head('/', baseSlowDown, entry.validate, errorChecking); export default router; \ No newline at end of file diff --git a/src/middleware/error.ts b/src/middleware/error.ts index 2eaf6e8..8db272e 100644 --- a/src/middleware/error.ts +++ b/src/middleware/error.ts @@ -17,8 +17,11 @@ export function notFound(req: Request, res: Response, next: NextFunction) { next(error); } -export function handler(err: Error, req: Request, res: Response, next: NextFunction) { - const statusCode = res.statusCode !== 200 ? res.statusCode : 500; +export function handler(err: HttpError, req: Request, res: Response, next: NextFunction) { + let statusCode = res.statusCode; + if (statusCode == 200) { + statusCode = err.statusCode || err.status || 500 + } res.status(statusCode); let message; diff --git a/src/middleware/limit.ts b/src/middleware/limit.ts index a73e6c8..eb9aea5 100644 --- a/src/middleware/limit.ts +++ b/src/middleware/limit.ts @@ -3,13 +3,11 @@ import { rateLimit, Options as rateLimiterOptions } from 'express-rate-limit'; import { slowDown, Options as slowDownOptions } from 'express-slow-down'; import logger from '@src/scripts/logger'; - /* ** configurations */ - const baseOptions: Partial = { - windowMs: 30 * 60 * 1000, + windowMs: 3 * 60 * 1000, skip: (req, res) => (res.locals.ip == "127.0.0.1" || res.locals.ip == "::1") } @@ -21,9 +19,17 @@ const baseSlowDownOptions: Partial = { const baseRateLimitOptions: Partial = { ...baseOptions, - limit: 10, // Limit each IP per window + limit: 50, // Limit each IP per window standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers + handler: function rateHandler(req: Request, res: Response, next: NextFunction, options: rateLimiterOptions) { + if (!Object.prototype.hasOwnProperty.call(ipsThatReachedLimit, res.locals.ip)) { + logger.error(`[RateLimit] reached ${req.originalUrl}, ${res.locals.ip}, ${req.get('User-Agent')}`); + ipsThatReachedLimit[res.locals.ip] = { limitReachedOnError: true, time: Date.now() }; + } + res.status(options.statusCode).send(options.message); + }, + message: "Too many requests" } @@ -46,14 +52,21 @@ setInterval(() => { */ export const baseSlowDown = slowDown(baseSlowDownOptions); +export const loginSlowDown = slowDown({ + ...baseSlowDownOptions, + delayAfter: 1, // no delay for amount of attempts + delayMs: (used: number) => (used - 1) * 250, // Add delay after delayAfter is reached + }); + +export const baseRateLimiter = rateLimit(baseRateLimitOptions); + export const errorRateLimiter = rateLimit({ ...baseRateLimitOptions, message: 'Too many requests with errors', - handler: (req: Request, res: Response, next: NextFunction, options: rateLimiterOptions) => { - if (!Object.prototype.hasOwnProperty.call(ipsThatReachedLimit, res.locals.ip)) { - logger.error(`[RateLimit] for invalid requests reached ${res.locals.ip}, ${req.get('User-Agent')}`); - ipsThatReachedLimit[res.locals.ip] = { limitReachedOnError: true, time: Date.now() }; - } - res.status(options.statusCode).send(options.message); - } +}); + +export const loginLimiter = rateLimit({ + ...baseRateLimitOptions, + limit: 3, + message: 'Too many attempts without valid login', }); \ No newline at end of file diff --git a/src/models/entry.ts b/src/models/entry.ts index 1f510bf..e7fc107 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -1,6 +1,6 @@ import { NextFunction, Request, Response } from 'express'; import { checkExact, query } from 'express-validator'; -import { crypt } from '@src/scripts/crypt'; +import { compare } from '@src/scripts/crypt'; import { create as createError } from '@src/middleware/error'; import * as file from '@src/scripts/file'; import { getTime } from '@src/scripts/time'; @@ -51,7 +51,7 @@ export const entry = { } } else { entries.push(entry); - } + } file.write(res, fileObj, next); @@ -102,10 +102,6 @@ export function checkTime(value: string) { throw new Error('Timestamp should represent a valid date'); } - if (process.env.NODE_ENV == "development") { - return true; // dev testing convenience - } - const now = new Date(); const difference = now.getTime() - date.getTime(); const oneDayInMilliseconds = 24 * 60 * 60 * 1000; @@ -117,23 +113,16 @@ export function checkTime(value: string) { } -function checkKey(value: string) { +async function checkKey(value: string) { + if (!value) { throw new Error('Key required'); } + if (!process.env.KEYB) { throw new Error('Configuration wrong'); } if (process.env.NODE_ENV != "production" && value == "test") { return true; // dev testing convenience } - if (!value) { - throw new Error('Key required'); - } - - value = decodeURIComponent(value); + const result = await compare(decodeURIComponent(value), process.env.KEYB); - const hash = crypt(value); - - if (process.env.KEYB != hash) { - if (process.env.NODE_ENV == "development") { - console.log(hash); - } + if (!result) { throw new Error('Key does not match'); } diff --git a/src/scripts/crypt.ts b/src/scripts/crypt.ts index b14ef42..1928c52 100644 --- a/src/scripts/crypt.ts +++ b/src/scripts/crypt.ts @@ -1,9 +1,20 @@ -import * as crypto from 'crypto'; +import * as bcrypt from "bcrypt"; +import crypto from "crypto"; -export const crypt = function (value:string) { +export const crypt = async function (password: string, quick = false) { + const extendedPassword = pepper(password); + return await bcrypt.hash(extendedPassword, quick ? 8 : 16); +}; + +export const compare = async function (password: string, hash: string) { + const extendedPassword = pepper(password); + return await bcrypt.compare(extendedPassword, hash) +} + +function pepper(password: string) { const key = process.env.KEYA; - if (!key) { - throw new Error('KEYA is not defined in the environment variables'); - } - return crypto.createHmac('sha256', key).update(value).digest("base64"); -}; \ No newline at end of file + if (!key) { throw new Error('KEYA is not defined in the environment variables'); } + return password + crypto.createHmac('sha256', key).digest("base64"); +} + + diff --git a/src/scripts/logger.ts b/src/scripts/logger.ts index 67effdd..ddd608a 100644 --- a/src/scripts/logger.ts +++ b/src/scripts/logger.ts @@ -10,17 +10,16 @@ if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } -// const logPath = path.resolve(__dirname, '../httpdocs/log', 'start.txt'); const date = new Date().toLocaleString('de-DE', { hour12: false }); export default { - log: (message: string | JSON, showDateInConsole: boolean = false, showLogInTest = false) => { + log: (message: string | JSON, showDateInConsole: boolean = false) => { message = JSON.stringify(message); fs.appendFileSync(logPath, `${date} \t|\t ${message} \n`); if (showDateInConsole) { message = `${chalk.dim(date + ":")} ${message}`; } - if (process.env.NODE_ENV == "development" || showLogInTest && process.env.NODE_ENV == "test") { + if (process.env.NODE_ENV != "production") { console.log(message); } }, @@ -46,7 +45,7 @@ export default { content = content.replace(prefix[0], chalk.red(prefix[0])); } } - console.error(content); // log string right away or processed Object + console.error(content); } } diff --git a/src/tests/app.test.ts b/src/tests/app.test.ts index b7c01c2..44ba08d 100644 --- a/src/tests/app.test.ts +++ b/src/tests/app.test.ts @@ -1,4 +1,10 @@ import axios from 'axios'; +import qs from 'qs'; + +// random data of 0.75Kb pre GZIP +const randomData = qs.stringify({ + randomData: 'zIakHvSaXDdLtaPaL02LhGr4Fk6hzXF7tELeR733YZyyye1fnjNzrSlHgqcHU8BKqvE5Mi4B7iHIEdqjTelpoWyaqXqX8l6LzOvROAkTF4lrLXLD1oMHwDL9hnjR0P7g0BB2DqagKkoEYD4TmXeAXT9PbevbirWnOEzmIgSv65SlsNTRFYhmzWl93twXEBNclHTCTnZpf6diWoo8FsXZR49pe9v8J1paalh2LlbNF4ZUxMxNpSvSTRHxvkYo0TMpd0NqUSSLduLIWcE1jhCWnmHhsbohDZjFfMhVS8IFvCiu7rxfuWgwMPqD9FcBR79eqJBy2tjDMqA9S1k9k50AkbOQ6USVfEuqOtocqXonTvC3Jml90KYSs0gX4SSTFHofpMtbWIdkuKqZbitQjsPSBpTx27dhFZd8zT4erdE1ltHnq83pjEj9hQYqatmdzQGYnOyh9YDt8i1IJpk4DX83DLzw3QhaFPgZFq98SOj4ILytmBMIqOtD464aF8PKGq6g7dVqYOtyF2FwyY0xgA7LjGaFzaCDjnGEcPIMRc2tcorsuRPKUI0zcde1gYPsn4WKaKUp87hJd1YtorzCXPfvivfGGL5v1XaSzApc9BbZpbxcpTOi4Pgvx7hNafUcaCr6kcjp4JVYSktnnGCwEplgGEF8uCELsEBUi9LNhgsnwgoRh55TaJfcaFfGfYLokXYEgiyOwYhhdEY3kfjHZWAyFS4owCR6nMJGOGMHrQi1fBefdp28PQGwgELix5Vf8j6P' +}); describe('Server Status', () => { it('The server is running', async () => { @@ -12,4 +18,25 @@ describe('Server Status', () => { expect(serverStatus).toBe(200); }) -}) \ No newline at end of file + + it('server is ignoring body on GET requests', async () => { + let serverStatus; + try { + const response = await axios.request({ + url: 'http://localhost:80/', + method: 'GET', + data: randomData, + }); + serverStatus = response.status; + } catch (error) { + console.error(error); + } + + expect(serverStatus).toBe(200); + }) + +}) + + + + diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index 6133c1d..2e189e0 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -1,4 +1,5 @@ import axios, { AxiosError } from 'axios'; +import qs from 'qs'; import fs from "fs"; import path from "path"; @@ -9,6 +10,7 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec params.set("timestamp", timestamp.toString()); url.search = params.toString(); + let response; if (expectStatus == 200) { if (method == "GET") { @@ -41,44 +43,67 @@ function isInRange(actual: string | number, expected: number, range: number) { return Math.abs(Number(actual) - expected) <= range; } +async function verifiedRequest(url: string, token: string) { + const response = await axios({ + method: 'get', + url: url, + headers: { + 'Authorization': `Bearer ${token}`, + } + }); + return response; +} + + + describe('HEAD /write', () => { + // eslint-disable-next-line jest/expect-expect it('with all parameters correctly set it should succeed', async () => { await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200); }); + // eslint-disable-next-line jest/expect-expect it('without key it sends 403', async () => { await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0", 403); }); + // eslint-disable-next-line jest/expect-expect it('with user length not equal to 2 it sends 422', async () => { await callServer(undefined, "user=x&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); }); + // eslint-disable-next-line jest/expect-expect it('with lat not between -90 and 90 it sends 422', async () => { await callServer(undefined, "user=xx&lat=91.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); }); + // eslint-disable-next-line jest/expect-expect it('with lon not between -180 and 180 it sends 422', async () => { await callServer(undefined, "user=xx&lat=45.000&lon=181.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); }); + // eslint-disable-next-line jest/expect-expect it('with timestamp to old sends 422', async () => { const timestamp = new Date().getTime() - 24 * 60 * 60 * 1000 * 2; // two days ago await callServer(timestamp, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); }) + // eslint-disable-next-line jest/expect-expect it('with hdop not between 0 and 100 it sends 422', async () => { await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422); }); + // eslint-disable-next-line jest/expect-expect it('with altitude not between 0 and 10000 it sends 422', async () => { await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test", 422); }); + // eslint-disable-next-line jest/expect-expect it('with speed not between 0 and 300 it sends 422', async () => { await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test", 422); }); + // eslint-disable-next-line jest/expect-expect it('with heading not between 0 and 360 it sends 422', async () => { await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test", 422); }); @@ -92,7 +117,7 @@ describe("GET /write", () => { const filePath = path.resolve(dirPath, `data-${formattedDate}.json`); it('there should a file of the current date', async () => { - await await callServer(undefined, "user=xx&lat=52.51451&lon=13.35105×tamp=R3Pl4C3&hdop=20.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + await callServer(undefined, "user=xx&lat=52.51451&lon=13.35105×tamp=R3Pl4C3&hdop=20.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); fs.access(filePath, fs.constants.F_OK, (err) => { expect(err).toBeFalsy(); @@ -176,13 +201,14 @@ describe("GET /write", () => { expect(entry.ignore).toBe(false); // current one to be false allways expect(lastEntry.ignore).toBe(true); // last one to high hdop to be true - await await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); jsonData = getData(filePath); entry = jsonData.entries[1]; // same data point, but not last now therefore ignore true expect(entry.ignore).toBe(true); }); }); + describe('API calls', () => { test(`1000 api calls`, async () => { for (let i = 0; i < 1000; i++) { @@ -203,15 +229,57 @@ describe('API calls', () => { }); -describe('/read', () => { - test(`returns json`, async () => { - const response = await axios.get("http://localhost:80/read?index=0"); +describe('read and login', () => { + let token = ""; + const testData = qs.stringify({ + user: "TEST", + password: "test", + }); + test(`redirect without logged in`, async () => { + try { + await axios.get("http://localhost:80/read/"); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + expect(axiosError.response.status).toBe(401); + } else { + console.error(axiosError); + } + } + }); + + it('test user can login', async () => { + const response = await axios.post('http://localhost:80/read/login', testData); + + expect(response.status).toBe(200); + expect(response.headers['content-type']).toEqual(expect.stringContaining('application/json')); + expect(response).toHaveProperty('data.token'); + expect(response.data.token).not.toBeNull(); + token = response.data.token; + }) + + test('wrong token get error', async () => { + try { + await verifiedRequest("http://localhost:80/read?index=0", "justWrongValue"); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + expect(axiosError.response.status).toBe(403); + } else { + console.error(axiosError); + } + } + }); + + test('verified request returns json', async () => { + const response = await verifiedRequest("http://localhost:80/read?index=0", token); expect(response.status).toBe(200); expect(response.headers['content-type']).toEqual(expect.stringContaining('application/json')); }); + test(`index parameter to long`, async () => { try { - await axios.get("http://localhost:80/read?index=1234"); + await verifiedRequest("http://localhost:80/read?index=1234", token); } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { @@ -221,9 +289,10 @@ describe('/read', () => { } } }); + test(`index parameter to be a number`, async () => { try { - await axios.get("http://localhost:80/read?index=a9"); + await verifiedRequest("http://localhost:80/read?index=a9", token); } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { @@ -234,7 +303,8 @@ describe('/read', () => { } }); test(`index parameter reduces length of json`, async () => { - const response = await axios.get("http://localhost:80/read?index=999"); + const response = await verifiedRequest("http://localhost:80/read?index=999", token); + expect(response.status).toBe(200); expect(response.data.entries.length).toBe(1); }); }); \ No newline at end of file diff --git a/src/tests/login.test.ts b/src/tests/login.test.ts new file mode 100644 index 0000000..3fefe2d --- /dev/null +++ b/src/tests/login.test.ts @@ -0,0 +1,58 @@ +import axios, { AxiosError } from 'axios'; +import qs from 'qs'; + +const userDataLarge = qs.stringify({ + user: "user", + password: "pass", + kilobyte: 'BPSwVu5vcvhWB17HcfIdyQK83mHJZKChv7zDihBJoifWK9EJFzK7VYf3kUgIqkc0io8DnSdewzc9U0GpzodQUFz0KLMaogsJruEbNSKvxnzUxS5UqSR64lLOmGumoPcn2InC0Ebpqfdiw90HFVZVlE3AY6Lhgbx8ILHi55RvpuGefDjBsePgow8Jh9sc8uVMCDglLmHQ0zk3PumMj0KlOszbMmX9fG0pPUsvLLc40biPBv9t97K3BFjYd3fGriRAQ3bFhGHBz2wzGbNQfHjKFDHuSvXOw8KReM7Wwd4Cl02QQ3RnDJVwH6cayh4BqFRXlP3i6uXw0l9qxdTv0q1CtV9rJho6zwo04gkGLvsS3AoYJQtHnOtUDdHPExu7l3nMKnPoRUwl7K2ePfHRuppFGqa43Q49bI04VjEhrB9k5S2uZJoxZdm63rIUrydmkZWdvBLVVZUIXwwIRnwLmoa26htKOz9FPKwWIPOM0NZj4jAoPhKqLDJwziNZn5UupzxBXoUM3BIyEk3K8GXs7eBduH9GCK2z2HPF0fJNtGiHASe7jCOC2mhSC5zGf9k0Yu1Ey63oQQZUtT7L57lp7UzPE2p6wzKDlbJZOn0Ho5OUfq3hE2C8fQRO1M6jDvRTiUIKhhxSHYd75Pvh4SG9lD8w5OHASusLDxmzKBUuG4GrGrQYpd0awJkqnKp5lk7psLD22YTtjTuDgI500tQLXSslxI1kIuB8RnN1LsxHyRQMVtXmNFOKKZV2U2frWpImIz2wSHCYrwRGygwDtiFfwtVwTapjhQqUMyb1vrWWi3EL1Y50fDCjDDHlvLI4N2tr2DULFf3a9m2SYWSoE6CYP4og5YyqjhqFQFm9urREInyZi9L0iQoMYxEqxTjGiVJfKmaSChSd0kQz6z2OdsxFbkMWJ2CAHOL1XNK8iFFSp93fIspaNMIonRVDCj4ZIP1LaPHDmIYcYTNU4k3Uz6VBHSIc1VjiG3sc2MZpKw9An0tJVlWbtVSk2RGYWIANAYyr5pQS' +}); +const userData = qs.stringify({ + user: "user", + password: "pass" +}); + +describe('Login', () => { + it('form available', async () => { + let serverStatus = {}; + let response = { data: "", status: "" }; + try { + response = await axios.get('http://localhost:80/read/login'); + serverStatus = response.status; + } catch (error) { + console.error(error); + } + + expect(serverStatus).toBe(200); + expect(response.data).toContain(' { + try { + await axios.post('http://localhost:80/read/login', userDataLarge); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + expect(axiosError.response.status).toBe(413); + } else { + console.error(axiosError); + } + } + }) + + it('invalid login verification test', async () => { + try { + await axios.post('http://localhost:80/read/login', userData); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + expect(axiosError.response.status).toBe(403); + } else { + console.error(axiosError); + } + } + }) +}) + + + + diff --git a/src/tests/entry.test.ts b/src/tests/unit.test.ts similarity index 96% rename from src/tests/entry.test.ts rename to src/tests/unit.test.ts index 48058e7..013a1b4 100644 --- a/src/tests/entry.test.ts +++ b/src/tests/unit.test.ts @@ -1,7 +1,7 @@ import { checkNumber, checkTime } from "../models/entry"; -describe("checkNumber", () => { +describe("entry checkNumber", () => { it("should throw error if value is not provided", () => { expect(() => checkNumber(0, 100)("")).toThrow(new Error('is required')); }); @@ -19,7 +19,7 @@ describe("checkNumber", () => { }); }); -describe("checkTime", () => { +describe("entry checkTime", () => { it("should throw error if value is not a number", () => { expect(() => checkTime("abc")).toThrow(new Error('Timestamp should be a number')); }); diff --git a/types.d.ts b/types.d.ts index 8931ce5..6bb5b4c 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ - namespace RateLimit { interface obj { [key: string]: { @@ -118,3 +117,8 @@ namespace Models { total: number } } + +interface HttpError extends Error { + status?: number; + statusCode?: number; +} \ No newline at end of file diff --git a/views/login-form.ejs b/views/login-form.ejs new file mode 100644 index 0000000..c628aab --- /dev/null +++ b/views/login-form.ejs @@ -0,0 +1,30 @@ + + + + + + Login Form - Lorex + + + + + + + + + + \ No newline at end of file From da13c770db9f9d229d3bf799b6e091e8c3e49dbd Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 21 Mar 2024 14:44:02 +0100 Subject: [PATCH 057/206] 48 move login to seperate controller (#49) * [Task] #43, add label to form * [Task] #48 login controller --- src/app.ts | 2 + src/controller/login.ts | 58 +++++++++++++++++++ src/controller/read.ts | 104 +--------------------------------- src/middleware/logged-in.ts | 13 +++++ src/scripts/token.ts | 49 ++++++++++++++++ src/tests/integration.test.ts | 2 +- src/tests/login.test.ts | 6 +- views/login-form.ejs | 5 +- 8 files changed, 131 insertions(+), 108 deletions(-) create mode 100644 src/controller/login.ts create mode 100644 src/middleware/logged-in.ts create mode 100644 src/scripts/token.ts diff --git a/src/app.ts b/src/app.ts index ce1edff..d7e1084 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,6 +9,7 @@ import cache from './middleware/cache'; import * as error from "./middleware/error"; import writeRouter from '@src/controller/write'; import readRouter from '@src/controller/read'; +import loginRouter from '@src/controller/login'; import path from 'path'; import logger from '@src/scripts/logger'; import { baseRateLimiter } from './middleware/limit'; @@ -57,6 +58,7 @@ app.get('/', (req, res) => { app.use('/write', writeRouter); app.use('/read', readRouter); +app.use('/login', loginRouter); // use httpdocs as static folder app.use('/', express.static(path.join(__dirname, 'httpdocs'), { diff --git a/src/controller/login.ts b/src/controller/login.ts new file mode 100644 index 0000000..7c71236 --- /dev/null +++ b/src/controller/login.ts @@ -0,0 +1,58 @@ +import express, { Request, Response, NextFunction } from 'express'; +import { create as createError } from '@src/middleware/error'; +import logger from '@src/scripts/logger'; +import { crypt, compare } from '@src/scripts/crypt'; +import { loginSlowDown, loginLimiter, baseSlowDown, baseRateLimiter } from '@src/middleware/limit'; +import { createToken } from '@src/scripts/token'; + +const router = express.Router(); + +router.get("/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response) { + res.locals.text = "start"; + loginLimiter(req, res, () => { + res.render("login-form"); + }); +}); + +router.post("/", loginSlowDown, async function postLogin(req: Request, res: Response, next: NextFunction) { + logger.log(req.body); + loginLimiter(req, res, async () => { + let validLogin = false; + const user = req.body.user; + const password = req.body.password; + let userFound = false; + if (!user || !password) { + return createError(res, 422, "Body does not contain all expected information", next); + } + + // Loop through all environment variables + for (const key in process.env) { + if (!key.startsWith('USER')) { continue; } + if (key.substring(5) == user) { + userFound = true; + const hash = process.env[key]; + if (hash) { + validLogin = await compare(password, hash); + } + } + } + + // only allow test user in test environment + if (user == "TEST" && validLogin && process.env.NODE_ENV == "production") { + validLogin = false; + } + + if (validLogin) { + const token = createToken(req, res); + res.json({ "token": token }); + } else { + if (!userFound) { + await crypt(password); // If no matching user is found, perform a dummy password comparison to prevent timing attacks + } + return createError(res, 403, `invalid login credentials`, next); + } + }); +}); + + +export default router; \ No newline at end of file diff --git a/src/controller/read.ts b/src/controller/read.ts index b04893d..bd4b88c 100644 --- a/src/controller/read.ts +++ b/src/controller/read.ts @@ -2,10 +2,7 @@ import express, { Request, Response, NextFunction } from 'express'; import * as file from '@src/scripts/file'; import { create as createError } from '@src/middleware/error'; import { validationResult, query } from 'express-validator'; -import jwt from 'jsonwebtoken'; -import logger from '@src/scripts/logger'; -import { crypt, compare } from '@src/scripts/crypt'; -import { loginSlowDown, loginLimiter, baseSlowDown, baseRateLimiter } from '@src/middleware/limit'; +import { isLoggedIn } from '@src/middleware/logged-in'; const router = express.Router(); @@ -35,105 +32,6 @@ router.get('/', res.json({ entries }); }); -router.get("/login/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response) { - res.locals.text = "start"; - loginLimiter(req, res, () => { - res.render("login-form"); - }); -}); - -router.post("/login/", loginSlowDown, async function postLogin(req: Request, res: Response, next: NextFunction) { - logger.log(req.body); - loginLimiter(req, res, async () => { - let validLogin = false; - const user = req.body.user; - const password = req.body.password; - let userFound = false; - if (!user || !password) { - return createError(res, 422, "Body does not contain all expected information", next); - } - - // Loop through all environment variables - for (const key in process.env) { - if (!key.startsWith('USER')) { continue; } - if (key.substring(5) == user) { - userFound = true; - const hash = process.env[key]; - if (hash) { - validLogin = await compare(password, hash); - } - } - } - - // only allow test user in test environment - if (user == "TEST" && validLogin && process.env.NODE_ENV == "production") { - validLogin = false; - } - - if (validLogin) { - const token = createToken(req, res); - res.json({ "token": token }); - } else { - if (!userFound) { - await crypt(password); // If no matching user is found, perform a dummy password comparison to prevent timing attacks - } - return createError(res, 403, `invalid login credentials`, next); - } - }); -}); - -function isLoggedIn(req: Request, res: Response, next: NextFunction) { - const result = validateToken(req); - if (!result.success) { - createError(res, result.status, result.message || "", next) - } else { - next(); - } -} - -function validateToken(req: Request) { - const key = process.env.KEYA; - const header = req.header('Authorization'); - const [type, token] = header ? header.split(' ') : ""; - let payload: string | jwt.JwtPayload = ""; - - // Guard; aka early return for common failures before verifying authorization - if (!key) { return { success: false, status: 500, message: 'Wrong Configuration' }; } - if (!header) { return { success: false, status: 401, message: 'No Authorization header' }; } - if (type !== 'Bearer' || !token) { return { success: false, status: 400, message: 'Invalid Authorization header' }; } - - try { - payload = jwt.verify(token, key); - } catch (err) { - let message = "could not verify"; - if (err instanceof Error) { - message = `${err.name} - ${err.message}`; - } - - return { success: false, status: 403, message: message }; - } - - // don't allow test user in production environment - if (typeof payload == "object" && payload.user == "TEST" && process.env.NODE_ENV == "production") { - return { success: false, status: 403, message: 'test user not allowed on production' }; - } - - return { success: true }; -} -function createToken(req: Request, res: Response) { - const key = process.env.KEYA; - if (!key) { throw new Error('Configuration is wrong'); } - const today = new Date(); - const dateString = today.toLocaleDateString("de-DE", { weekday: "short", year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); - const payload = { - date: dateString, - user: req.body.user - }; - const token = jwt.sign(payload, key, { expiresIn: 60 * 2 }); - res.locals.token = token; - logger.log(JSON.stringify(payload), true); - return token; -} export default router; diff --git a/src/middleware/logged-in.ts b/src/middleware/logged-in.ts new file mode 100644 index 0000000..eddca04 --- /dev/null +++ b/src/middleware/logged-in.ts @@ -0,0 +1,13 @@ +import { Request, Response, NextFunction } from 'express'; +import { validateToken } from '@src/scripts/token'; +import { create as createError } from '@src/middleware/error'; + + +export function isLoggedIn(req: Request, res: Response, next: NextFunction) { + const result = validateToken(req); + if (!result.success) { + createError(res, result.status, result.message || "", next) + } else { + next(); + } +} diff --git a/src/scripts/token.ts b/src/scripts/token.ts new file mode 100644 index 0000000..f26a718 --- /dev/null +++ b/src/scripts/token.ts @@ -0,0 +1,49 @@ +import jwt from 'jsonwebtoken'; +import logger from '@src/scripts/logger'; +import {Request, Response } from 'express'; + + +export function validateToken(req: Request) { + const key = process.env.KEYA; + const header = req.header('Authorization'); + const [type, token] = header ? header.split(' ') : ""; + let payload: string | jwt.JwtPayload = ""; + + // Guard; aka early return for common failures before verifying authorization + if (!key) { return { success: false, status: 500, message: 'Wrong Configuration' }; } + if (!header) { return { success: false, status: 401, message: 'No Authorization header' }; } + if (type !== 'Bearer' || !token) { return { success: false, status: 400, message: 'Invalid Authorization header' }; } + + try { + payload = jwt.verify(token, key); + } catch (err) { + let message = "could not verify"; + if (err instanceof Error) { + message = `${err.name} - ${err.message}`; + } + + return { success: false, status: 403, message: message }; + } + + // don't allow test user in production environment + if (typeof payload == "object" && payload.user == "TEST" && process.env.NODE_ENV == "production") { + return { success: false, status: 403, message: 'test user not allowed on production' }; + } + + return { success: true }; +} + +export function createToken(req: Request, res: Response) { + const key = process.env.KEYA; + if (!key) { throw new Error('Configuration is wrong'); } + const today = new Date(); + const dateString = today.toLocaleDateString("de-DE", { weekday: "short", year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); + const payload = { + date: dateString, + user: req.body.user + }; + const token = jwt.sign(payload, key, { expiresIn: 60 * 2 }); + res.locals.token = token; + logger.log(JSON.stringify(payload), true); + return token; +} diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index 2e189e0..4fd5f4e 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -249,7 +249,7 @@ describe('read and login', () => { }); it('test user can login', async () => { - const response = await axios.post('http://localhost:80/read/login', testData); + const response = await axios.post('http://localhost:80/login', testData); expect(response.status).toBe(200); expect(response.headers['content-type']).toEqual(expect.stringContaining('application/json')); diff --git a/src/tests/login.test.ts b/src/tests/login.test.ts index 3fefe2d..6fbc340 100644 --- a/src/tests/login.test.ts +++ b/src/tests/login.test.ts @@ -16,7 +16,7 @@ describe('Login', () => { let serverStatus = {}; let response = { data: "", status: "" }; try { - response = await axios.get('http://localhost:80/read/login'); + response = await axios.get('http://localhost:80/login'); serverStatus = response.status; } catch (error) { console.error(error); @@ -28,7 +28,7 @@ describe('Login', () => { it('server is blocking requests with large body', async () => { try { - await axios.post('http://localhost:80/read/login', userDataLarge); + await axios.post('http://localhost:80/login', userDataLarge); } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { @@ -41,7 +41,7 @@ describe('Login', () => { it('invalid login verification test', async () => { try { - await axios.post('http://localhost:80/read/login', userData); + await axios.post('http://localhost:80/login', userData); } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { diff --git a/views/login-form.ejs b/views/login-form.ejs index c628aab..802a7c9 100644 --- a/views/login-form.ejs +++ b/views/login-form.ejs @@ -20,7 +20,10 @@ Password: - +

Token: <%= locals.token %>

From 8ab8cba3b7762b9f1c53cad77df37a1654a9343b Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 26 Mar 2024 12:32:13 +0100 Subject: [PATCH 058/206] 50 integrate csrf protection for login form (#53) * [Task] #50, create CSRF Validation for login form * [Task] #43, added icon to repository for later use * [Task] #50, cleanup cetntralized; rename token functions * [Task] #50, reduced token length and improved error handling * [Task] #50 csrf tests added to login * [Task] #50, added test case for csrf, repaired integration --- httpdocs/icon.png | Bin 0 -> 50481 bytes src/app.ts | 13 ++++++--- src/controller/login.ts | 20 +++++++------- src/middleware/limit.ts | 37 +++++++++++++------------- src/middleware/logged-in.ts | 4 +-- src/scripts/crypt.ts | 2 -- src/scripts/token.ts | 48 ++++++++++++++++++++++++++++++---- src/tests/integration.test.ts | 21 ++++++++++++--- src/tests/login.test.ts | 42 ++++++++++++++++++++++++++--- types.d.ts | 5 ++++ views/login-form.ejs | 6 ++--- 11 files changed, 147 insertions(+), 51 deletions(-) create mode 100644 httpdocs/icon.png diff --git a/httpdocs/icon.png b/httpdocs/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..827ec59bfce7546e32bc5aaa563e86ee6a924cc9 GIT binary patch literal 50481 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 VlY&WEpLqlPWF!?nSAH@K`aiw_K*0b2 literal 0 HcmV?d00001 diff --git a/src/app.ts b/src/app.ts index d7e1084..b11cfdb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,7 +12,8 @@ import readRouter from '@src/controller/read'; import loginRouter from '@src/controller/login'; import path from 'path'; import logger from '@src/scripts/logger'; -import { baseRateLimiter } from './middleware/limit'; +import { baseRateLimiter, cleanup as cleanupRateLimitedIps } from './middleware/limit'; +import { cleanupCSRF } from "@src/scripts/token"; // configurations config(); // dotenv @@ -43,8 +44,8 @@ app.use(compression()) app.use(hpp()); app.use(baseRateLimiter); app.use((req, res, next) => { // limit body for specific http methods - if(['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) { - return express.urlencoded({ limit: '0.5kb', extended: true })(req, res, next); + if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) { + return express.urlencoded({ limit: '0.5kb', extended: true })(req, res, next); } next(); }); @@ -75,6 +76,12 @@ const server = app.listen(80, () => { logger.log(`Server running //localhost:80, ENV: ${process.env.NODE_ENV}`, true); }); +// scheduled cleanup +setInterval(() => { + cleanupCSRF(); + cleanupRateLimitedIps(); +}, 1000 * 60 * 5); + // catching shutdowns ['SIGINT', 'SIGTERM', 'exit'].forEach((signal) => { process.on(signal, () => { diff --git a/src/controller/login.ts b/src/controller/login.ts index 7c71236..6a47f4c 100644 --- a/src/controller/login.ts +++ b/src/controller/login.ts @@ -1,29 +1,29 @@ import express, { Request, Response, NextFunction } from 'express'; import { create as createError } from '@src/middleware/error'; -import logger from '@src/scripts/logger'; import { crypt, compare } from '@src/scripts/crypt'; import { loginSlowDown, loginLimiter, baseSlowDown, baseRateLimiter } from '@src/middleware/limit'; -import { createToken } from '@src/scripts/token'; +import { createJWT, createCSRF, validateCSRF } from '@src/scripts/token'; + const router = express.Router(); -router.get("/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response) { - res.locals.text = "start"; +router.get("/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response, next: NextFunction) { loginLimiter(req, res, () => { + const csrfToken = createCSRF(res, next); + res.locals = {...res.locals, text: 'start', csrfToken: csrfToken}; res.render("login-form"); }); }); router.post("/", loginSlowDown, async function postLogin(req: Request, res: Response, next: NextFunction) { - logger.log(req.body); loginLimiter(req, res, async () => { let validLogin = false; + const token = req.body.csrfToken; const user = req.body.user; const password = req.body.password; let userFound = false; - if (!user || !password) { - return createError(res, 422, "Body does not contain all expected information", next); - } + if (!user || !password) { return createError(res, 422, "Body does not contain all expected information", next); } + if (!token || !validateCSRF(req.body.csrfToken)) { return createError(res, 403, "Invalid CSRF Token", next); } // Loop through all environment variables for (const key in process.env) { @@ -43,13 +43,13 @@ router.post("/", loginSlowDown, async function postLogin(req: Request, res: Resp } if (validLogin) { - const token = createToken(req, res); + const token = createJWT(req, res); res.json({ "token": token }); } else { if (!userFound) { await crypt(password); // If no matching user is found, perform a dummy password comparison to prevent timing attacks } - return createError(res, 403, `invalid login credentials`, next); + return createError(res, 403, `Invalid credentials`, next); } }); }); diff --git a/src/middleware/limit.ts b/src/middleware/limit.ts index eb9aea5..1301071 100644 --- a/src/middleware/limit.ts +++ b/src/middleware/limit.ts @@ -3,6 +3,8 @@ import { rateLimit, Options as rateLimiterOptions } from 'express-rate-limit'; import { slowDown, Options as slowDownOptions } from 'express-slow-down'; import logger from '@src/scripts/logger'; +const ipsThatReachedLimit: RateLimit.obj = {}; // prevent logs from flooding + /* ** configurations */ @@ -33,30 +35,17 @@ const baseRateLimitOptions: Partial = { } -/* -** cleanup -*/ -const ipsThatReachedLimit: RateLimit.obj = {}; // prevent logs from flooding -setInterval(() => { - const oneHourAgo = Date.now() - 60 * 60 * 1000; - for (const ip in ipsThatReachedLimit) { - if (ipsThatReachedLimit[ip].time < oneHourAgo) { - delete ipsThatReachedLimit[ip]; - } - } -}, 60 * 60 * 1000); - /* ** exported section */ export const baseSlowDown = slowDown(baseSlowDownOptions); -export const loginSlowDown = slowDown({ - ...baseSlowDownOptions, - delayAfter: 1, // no delay for amount of attempts - delayMs: (used: number) => (used - 1) * 250, // Add delay after delayAfter is reached - }); +export const loginSlowDown = slowDown({ + ...baseSlowDownOptions, + delayAfter: 1, // no delay for amount of attempts + delayMs: (used: number) => (used - 1) * 250, // Add delay after delayAfter is reached +}); export const baseRateLimiter = rateLimit(baseRateLimitOptions); @@ -69,4 +58,14 @@ export const loginLimiter = rateLimit({ ...baseRateLimitOptions, limit: 3, message: 'Too many attempts without valid login', -}); \ No newline at end of file +}); + + +export function cleanup() { + const oneHourAgo = Date.now() - 60 * 60 * 1000; + for (const ip in ipsThatReachedLimit) { + if (ipsThatReachedLimit[ip].time < oneHourAgo) { + delete ipsThatReachedLimit[ip]; + } + } +} \ No newline at end of file diff --git a/src/middleware/logged-in.ts b/src/middleware/logged-in.ts index eddca04..f6546ba 100644 --- a/src/middleware/logged-in.ts +++ b/src/middleware/logged-in.ts @@ -1,10 +1,10 @@ import { Request, Response, NextFunction } from 'express'; -import { validateToken } from '@src/scripts/token'; +import { validateJWT } from '@src/scripts/token'; import { create as createError } from '@src/middleware/error'; export function isLoggedIn(req: Request, res: Response, next: NextFunction) { - const result = validateToken(req); + const result = validateJWT(req); if (!result.success) { createError(res, result.status, result.message || "", next) } else { diff --git a/src/scripts/crypt.ts b/src/scripts/crypt.ts index 1928c52..6bef0df 100644 --- a/src/scripts/crypt.ts +++ b/src/scripts/crypt.ts @@ -16,5 +16,3 @@ function pepper(password: string) { if (!key) { throw new Error('KEYA is not defined in the environment variables'); } return password + crypto.createHmac('sha256', key).digest("base64"); } - - diff --git a/src/scripts/token.ts b/src/scripts/token.ts index f26a718..f9f4d7a 100644 --- a/src/scripts/token.ts +++ b/src/scripts/token.ts @@ -1,9 +1,48 @@ import jwt from 'jsonwebtoken'; -import logger from '@src/scripts/logger'; -import {Request, Response } from 'express'; +import { NextFunction, Request, Response } from 'express'; +import crypto from 'crypto'; +import { create as createError } from '@src/middleware/error'; -export function validateToken(req: Request) { +const csrfTokens: Set = new Set(); + +export function createCSRF(res: Response, next: NextFunction): string { + if (csrfTokens.size > 100) { // Max Number of Tokens in memory + res.set('Retry-After', '300'); // 5 minutes + createError(res, 503, "Too many tokens", next); + } + + const token = crypto.randomBytes(16).toString('hex'); + const expiry = Date.now() + (5 * 60 * 1000); // Token expires in 5 minutes + const csrfToken: CSRFToken = { token, expiry }; + csrfTokens.add(csrfToken); + + return token; +} + +export function validateCSRF(token: string): boolean { + const currentTime = Date.now(); + let valid: boolean = false; + for (const entry of csrfTokens) { + if (entry.token === token) { + valid = entry.expiry > currentTime; + csrfTokens.delete(entry); + } + } + + return valid; +} + +export function cleanupCSRF() { + const currentTime = Date.now(); + for (const entry of csrfTokens) { + if (entry.expiry < currentTime) { + csrfTokens.delete(entry); + } + } +} + +export function validateJWT(req: Request) { const key = process.env.KEYA; const header = req.header('Authorization'); const [type, token] = header ? header.split(' ') : ""; @@ -33,7 +72,7 @@ export function validateToken(req: Request) { return { success: true }; } -export function createToken(req: Request, res: Response) { +export function createJWT(req: Request, res: Response) { const key = process.env.KEYA; if (!key) { throw new Error('Configuration is wrong'); } const today = new Date(); @@ -44,6 +83,5 @@ export function createToken(req: Request, res: Response) { }; const token = jwt.sign(payload, key, { expiresIn: 60 * 2 }); res.locals.token = token; - logger.log(JSON.stringify(payload), true); return token; } diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index 4fd5f4e..067d3ea 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -231,10 +231,25 @@ describe('API calls', () => { describe('read and login', () => { let token = ""; - const testData = qs.stringify({ + const testData = { user: "TEST", password: "test", - }); + csrfToken: "" + } + + it('form available / get Token', async () => { + let response = {data:""}; + try { + response = await axios.get('http://localhost:80/login'); + } catch (error) { + console.error(error); + } + + const regex = /name="csrfToken" value="([^"]*)"/; + const match = response.data.match(regex); + testData.csrfToken = match ? match[1] : '-'; + }) + test(`redirect without logged in`, async () => { try { await axios.get("http://localhost:80/read/"); @@ -249,7 +264,7 @@ describe('read and login', () => { }); it('test user can login', async () => { - const response = await axios.post('http://localhost:80/login', testData); + const response = await axios.post('http://localhost:80/login', qs.stringify(testData)); expect(response.status).toBe(200); expect(response.headers['content-type']).toEqual(expect.stringContaining('application/json')); diff --git a/src/tests/login.test.ts b/src/tests/login.test.ts index 6fbc340..e177826 100644 --- a/src/tests/login.test.ts +++ b/src/tests/login.test.ts @@ -6,11 +6,18 @@ const userDataLarge = qs.stringify({ password: "pass", kilobyte: 'BPSwVu5vcvhWB17HcfIdyQK83mHJZKChv7zDihBJoifWK9EJFzK7VYf3kUgIqkc0io8DnSdewzc9U0GpzodQUFz0KLMaogsJruEbNSKvxnzUxS5UqSR64lLOmGumoPcn2InC0Ebpqfdiw90HFVZVlE3AY6Lhgbx8ILHi55RvpuGefDjBsePgow8Jh9sc8uVMCDglLmHQ0zk3PumMj0KlOszbMmX9fG0pPUsvLLc40biPBv9t97K3BFjYd3fGriRAQ3bFhGHBz2wzGbNQfHjKFDHuSvXOw8KReM7Wwd4Cl02QQ3RnDJVwH6cayh4BqFRXlP3i6uXw0l9qxdTv0q1CtV9rJho6zwo04gkGLvsS3AoYJQtHnOtUDdHPExu7l3nMKnPoRUwl7K2ePfHRuppFGqa43Q49bI04VjEhrB9k5S2uZJoxZdm63rIUrydmkZWdvBLVVZUIXwwIRnwLmoa26htKOz9FPKwWIPOM0NZj4jAoPhKqLDJwziNZn5UupzxBXoUM3BIyEk3K8GXs7eBduH9GCK2z2HPF0fJNtGiHASe7jCOC2mhSC5zGf9k0Yu1Ey63oQQZUtT7L57lp7UzPE2p6wzKDlbJZOn0Ho5OUfq3hE2C8fQRO1M6jDvRTiUIKhhxSHYd75Pvh4SG9lD8w5OHASusLDxmzKBUuG4GrGrQYpd0awJkqnKp5lk7psLD22YTtjTuDgI500tQLXSslxI1kIuB8RnN1LsxHyRQMVtXmNFOKKZV2U2frWpImIz2wSHCYrwRGygwDtiFfwtVwTapjhQqUMyb1vrWWi3EL1Y50fDCjDDHlvLI4N2tr2DULFf3a9m2SYWSoE6CYP4og5YyqjhqFQFm9urREInyZi9L0iQoMYxEqxTjGiVJfKmaSChSd0kQz6z2OdsxFbkMWJ2CAHOL1XNK8iFFSp93fIspaNMIonRVDCj4ZIP1LaPHDmIYcYTNU4k3Uz6VBHSIc1VjiG3sc2MZpKw9An0tJVlWbtVSk2RGYWIANAYyr5pQS' }); -const userData = qs.stringify({ +const userDataWithoutToken = qs.stringify({ user: "user", password: "pass" }); +let csrfToken = "-"; +const userDataWithToken = { + user: "user", + password: "pass", + csrfToken: "" +}; + describe('Login', () => { it('form available', async () => { let serverStatus = {}; @@ -24,6 +31,10 @@ describe('Login', () => { expect(serverStatus).toBe(200); expect(response.data).toContain(' { @@ -39,13 +50,38 @@ describe('Login', () => { } }) - it('invalid login verification test', async () => { + it('invalid csrf shows correct error', async () => { + try { + await axios.post('http://localhost:80/login', userDataWithoutToken); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + expect(axiosError.response.status).toBe(403); + if (axiosError.response.data) { + expect(JSON.stringify(axiosError.response.data)).toContain('Invalid CSRF'); + } else { + throw Error("fail"); + } + } else { + console.error(axiosError); + } + } + }) + + + it('test invalid credentials to return error', async () => { try { - await axios.post('http://localhost:80/login', userData); + userDataWithToken.csrfToken = csrfToken + await axios.post('http://localhost:80/login', qs.stringify(userDataWithToken)); } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { expect(axiosError.response.status).toBe(403); + if (axiosError.response.data) { + expect(JSON.stringify(axiosError.response.data)).toContain('Invalid credentials'); + } else { + throw Error("fail"); + } } else { console.error(axiosError); } diff --git a/types.d.ts b/types.d.ts index 6bb5b4c..3cde1ce 100644 --- a/types.d.ts +++ b/types.d.ts @@ -118,6 +118,11 @@ namespace Models { } } +interface CSRFToken { + token: string; + expiry: number; +} + interface HttpError extends Error { status?: number; statusCode?: number; diff --git a/views/login-form.ejs b/views/login-form.ejs index 802a7c9..34bf551 100644 --- a/views/login-form.ejs +++ b/views/login-form.ejs @@ -10,7 +10,7 @@ - From 1018d2e10876b92dfe1c916ab938d262fe9a00e5 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 26 Mar 2024 12:35:24 +0100 Subject: [PATCH 059/206] fix: upgrade express-rate-limit from 7.1.5 to 7.2.0 (#52) Snyk has created this PR to upgrade express-rate-limit from 7.1.5 to 7.2.0. See this package in npm: https://www.npmjs.com/package/express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index bae40f0..0cce386 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "compression": "^1.7.4", "ejs": "^3.1.9", "express": "^4.18.2", - "express-rate-limit": "^7.1.5", + "express-rate-limit": "^7.2.0", "express-slow-down": "^2.0.1", "express-validator": "^7.0.1", "helmet": "^7.1.0", @@ -3711,9 +3711,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.5.tgz", - "integrity": "sha512-/iVogxu7ueadrepw1bS0X0kaRC/U0afwiYRSLg68Ts+p4Dc85Q5QKsOnPS/QUjPMHvOJQtBDrZgvkOzf8ejUYw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.2.0.tgz", + "integrity": "sha512-T7nul1t4TNyfZMJ7pKRKkdeVJWa2CqB8NA1P8BwYaoDI5QSBZARv5oMS43J7b7I5P+4asjVXjb7ONuwDKucahg==", "engines": { "node": ">= 16" }, diff --git a/package.json b/package.json index d42439d..cd90a10 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "compression": "^1.7.4", "ejs": "^3.1.9", "express": "^4.18.2", - "express-rate-limit": "^7.1.5", + "express-rate-limit": "^7.2.0", "express-slow-down": "^2.0.1", "express-validator": "^7.0.1", "helmet": "^7.1.0", From eb76667023e5640d1067d562cffe9b782dbb2a08 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 26 Mar 2024 12:49:50 +0100 Subject: [PATCH 060/206] fix: package.json & package-lock.json to reduce vulnerabilities (#54) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-EXPRESS-6474509 Co-authored-by: snyk-bot --- package-lock.json | 90 +++++++++++++++++++++++++---------------------- package.json | 2 +- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0cce386..471409b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "chalk": "^4.1.2", "compression": "^1.7.4", "ejs": "^3.1.9", - "express": "^4.18.2", + "express": "^4.19.2", "express-rate-limit": "^7.2.0", "express-slow-down": "^2.0.1", "express-validator": "^7.0.1", @@ -2439,12 +2439,12 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -2452,7 +2452,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -2461,20 +2461,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2917,9 +2903,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -3670,16 +3656,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -4191,9 +4177,9 @@ } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "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" }, @@ -6199,6 +6185,20 @@ "node": ">= 0.6" } }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -6449,16 +6449,16 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "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.2", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6500,13 +6500,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "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.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "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" diff --git a/package.json b/package.json index cd90a10..e27991c 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "chalk": "^4.1.2", "compression": "^1.7.4", "ejs": "^3.1.9", - "express": "^4.18.2", + "express": "^4.19.2", "express-rate-limit": "^7.2.0", "express-slow-down": "^2.0.1", "express-validator": "^7.0.1", From ff66e39438c8acad89e3068fec31e770762df7ad Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 26 Mar 2024 12:53:12 +0100 Subject: [PATCH 061/206] [Snyk] Upgrade express from 4.18.2 to 4.18.3 (#51) * fix: upgrade express from 4.18.2 to 4.18.3 Snyk has created this PR to upgrade express from 4.18.2 to 4.18.3. See this package in npm: https://www.npmjs.com/package/express See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr * 50 integrate csrf protection for login form (#53) * [Task] #50, create CSRF Validation for login form * [Task] #43, added icon to repository for later use * [Task] #50, cleanup cetntralized; rename token functions * [Task] #50, reduced token length and improved error handling * [Task] #50 csrf tests added to login * [Task] #50, added test case for csrf, repaired integration * fix: upgrade express-rate-limit from 7.1.5 to 7.2.0 (#52) Snyk has created this PR to upgrade express-rate-limit from 7.1.5 to 7.2.0. See this package in npm: https://www.npmjs.com/package/express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --------- Co-authored-by: snyk-bot From 5d616b43b478f65d5363dc8fe2bdfd52fa2e5777 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 28 Mar 2024 16:37:14 +0100 Subject: [PATCH 062/206] [Task] update dev after main merge --- package-lock.json | 2792 +++++++++++++++++++++++++++++++++- package.json | 6 +- src/app.test.ts | 15 - src/app.ts | 2 +- src/controller/write.test.ts | 100 -- src/models/entry.test.ts | 47 - tsconfig.json | 1 + 7 files changed, 2790 insertions(+), 173 deletions(-) delete mode 100644 src/app.test.ts delete mode 100644 src/controller/write.test.ts delete mode 100644 src/models/entry.test.ts diff --git a/package-lock.json b/package-lock.json index 471409b..2953ac7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,14 +24,14 @@ }, "devDependencies": { "@jest/globals": "^29.7.0", - "@tsconfig/node20": "^20.1.2", + "@tsconfig/node20": "^20.1.4", "@types/bcrypt": "^5.0.2", "@types/compression": "^1.7.5", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.6", - "@types/node": "^20.10.6", + "@types/node": "^20.11.30", "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", @@ -40,8 +40,10 @@ "dotenv": "^16.3.1", "eslint": "^8.56.0", "eslint-plugin-jest": "^27.6.3", + "install": "^0.13.0", "jest": "^29.7.0", "nodemon": "^3.0.2", + "npm": "^10.5.0", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "typescript": "^5.3.3" @@ -1525,9 +1527,9 @@ "dev": true }, "node_modules/@tsconfig/node20": { - "version": "20.1.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.2.tgz", - "integrity": "sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==", + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.4.tgz", + "integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==", "dev": true }, "node_modules/@types/babel__core": { @@ -1712,9 +1714,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", - "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -4381,6 +4383,15 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -5734,6 +5745,163 @@ "node": ">=0.10.0" } }, + "node_modules/npm": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.5.0.tgz", + "integrity": "sha512-Ejxwvfh9YnWVU2yA5FzoYLTW52vxHCz+MHrOFg9Cc8IFgF/6f5AGPAvb5WTay5DIUP1NIfN3VBZ0cLlGO0Ys+A==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "cli-table3", + "columnify", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "npmlog", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "dev": true, + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^7.2.1", + "@npmcli/config": "^8.0.2", + "@npmcli/fs": "^3.1.0", + "@npmcli/map-workspaces": "^3.0.4", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.1", + "@npmcli/run-script": "^7.0.4", + "@sigstore/tuf": "^2.3.1", + "abbrev": "^2.0.0", + "archy": "~1.0.0", + "cacache": "^18.0.2", + "chalk": "^5.3.0", + "ci-info": "^4.0.0", + "cli-columns": "^4.0.0", + "cli-table3": "^0.6.3", + "columnify": "^1.6.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^7.0.1", + "ini": "^4.1.1", + "init-package-json": "^6.0.0", + "is-cidr": "^5.0.3", + "json-parse-even-better-errors": "^3.0.1", + "libnpmaccess": "^8.0.1", + "libnpmdiff": "^6.0.3", + "libnpmexec": "^7.0.4", + "libnpmfund": "^5.0.1", + "libnpmhook": "^10.0.0", + "libnpmorg": "^6.0.1", + "libnpmpack": "^6.0.3", + "libnpmpublish": "^9.0.2", + "libnpmsearch": "^7.0.0", + "libnpmteam": "^6.0.0", + "libnpmversion": "^5.0.1", + "make-fetch-happen": "^13.0.0", + "minimatch": "^9.0.3", + "minipass": "^7.0.4", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^10.0.1", + "nopt": "^7.2.0", + "normalize-package-data": "^6.0.0", + "npm-audit-report": "^5.0.0", + "npm-install-checks": "^6.3.0", + "npm-package-arg": "^11.0.1", + "npm-pick-manifest": "^9.0.0", + "npm-profile": "^9.0.0", + "npm-registry-fetch": "^16.1.0", + "npm-user-validate": "^2.0.0", + "npmlog": "^7.0.1", + "p-map": "^4.0.0", + "pacote": "^17.0.6", + "parse-conflict-json": "^3.0.1", + "proc-log": "^3.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^2.1.0", + "semver": "^7.6.0", + "spdx-expression-parse": "^3.0.1", + "ssri": "^10.0.5", + "supports-color": "^9.4.0", + "tar": "^6.2.0", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^5.0.0", + "which": "^4.0.0", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -5746,6 +5914,2614 @@ "node": ">=8" } }, + "node_modules/npm/node_modules/@colors/colors": { + "version": "1.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "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/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "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/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "7.4.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^3.1.0", + "@npmcli/installed-package-contents": "^2.0.2", + "@npmcli/map-workspaces": "^3.0.2", + "@npmcli/metavuln-calculator": "^7.0.0", + "@npmcli/name-from-folder": "^2.0.0", + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/query": "^3.1.0", + "@npmcli/run-script": "^7.0.2", + "bin-links": "^4.0.1", + "cacache": "^18.0.0", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^7.0.1", + "json-parse-even-better-errors": "^3.0.0", + "json-stringify-nice": "^1.1.4", + "minimatch": "^9.0.0", + "nopt": "^7.0.0", + "npm-install-checks": "^6.2.0", + "npm-package-arg": "^11.0.1", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^16.0.0", + "npmlog": "^7.0.1", + "pacote": "^17.0.4", + "parse-conflict-json": "^3.0.0", + "proc-log": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^10.0.5", + "treeverse": "^3.0.0", + "walk-up-path": "^3.0.1" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "8.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^3.0.2", + "ci-info": "^4.0.0", + "ini": "^4.1.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/disparity-colors": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ansi-styles": "^4.3.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/disparity-colors/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "5.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "lib/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^2.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^18.0.0", + "json-parse-even-better-errors": "^3.0.0", + "pacote": "^17.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "7.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/core": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.3.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "2.2.3", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.2.0", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.0", + "make-fetch-happen": "^13.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "2.3.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.0", + "tuf-js": "^2.2.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.2.0", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/are-we-there-yet": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/builtins": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "18.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.0.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^5.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cli-table3": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/npm/node_modules/clone": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/color-support": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/npm/node_modules/columnify": { + "version": "1.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/console-control-strings": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/defaults": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/diff": { + "version": "5.2.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/gauge": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "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": "^4.0.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "10.3.10", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/has-unicode": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hasown": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "6.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^11.0.0", + "promzard": "^1.0.0", + "read": "^2.0.0", + "read-package-json": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "9.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "5.0.3", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "4.0.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/is-core-module": { + "version": "2.13.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-lambda": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "2.3.6", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/jsbn": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^11.0.1", + "npm-registry-fetch": "^16.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "6.0.7", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.2.1", + "@npmcli/disparity-colors": "^3.0.0", + "@npmcli/installed-package-contents": "^2.0.2", + "binary-extensions": "^2.2.0", + "diff": "^5.1.0", + "minimatch": "^9.0.0", + "npm-package-arg": "^11.0.1", + "pacote": "^17.0.4", + "tar": "^6.2.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "7.0.8", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.2.1", + "@npmcli/run-script": "^7.0.2", + "ci-info": "^4.0.0", + "npm-package-arg": "^11.0.1", + "npmlog": "^7.0.1", + "pacote": "^17.0.4", + "proc-log": "^3.0.0", + "read": "^2.0.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "5.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "10.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^16.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^16.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "6.0.7", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.2.1", + "@npmcli/run-script": "^7.0.2", + "npm-package-arg": "^11.0.1", + "pacote": "^17.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "9.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ci-info": "^4.0.0", + "normalize-package-data": "^6.0.0", + "npm-package-arg": "^11.0.1", + "npm-registry-fetch": "^16.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.7", + "sigstore": "^2.2.0", + "ssri": "^10.0.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^16.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^16.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "5.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.3", + "@npmcli/run-script": "^7.0.2", + "json-parse-even-better-errors": "^3.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "10.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "13.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "9.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-json-stream": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/npm/node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "10.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "7.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "6.3.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "11.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^16.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "16.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npmlog": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^4.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^5.0.0", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/pacote": { + "version": "17.0.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^7.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^16.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^7.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.10.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "6.0.15", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~1.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.6.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "2.2.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.2.0", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.0", + "@sigstore/sign": "^2.2.3", + "@sigstore/tuf": "^2.3.1", + "@sigstore/verify": "^1.1.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 16.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "dev": true, + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.17", + "dev": true, + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "10.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.2.0", + "dev": true, + "inBundle": true, + "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/npm/node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "2.0.0", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/wcwidth": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/npm/node_modules/which": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/which/node_modules/isexe": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/npm/node_modules/wide-align": { + "version": "1.1.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "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/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "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/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "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/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", diff --git a/package.json b/package.json index e27991c..4e3216f 100644 --- a/package.json +++ b/package.json @@ -24,14 +24,14 @@ "author": "Type-Style", "devDependencies": { "@jest/globals": "^29.7.0", - "@tsconfig/node20": "^20.1.2", + "@tsconfig/node20": "^20.1.4", "@types/bcrypt": "^5.0.2", "@types/compression": "^1.7.5", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.6", - "@types/node": "^20.10.6", + "@types/node": "^20.11.30", "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", @@ -40,8 +40,10 @@ "dotenv": "^16.3.1", "eslint": "^8.56.0", "eslint-plugin-jest": "^27.6.3", + "install": "^0.13.0", "jest": "^29.7.0", "nodemon": "^3.0.2", + "npm": "^10.5.0", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "typescript": "^5.3.3" diff --git a/src/app.test.ts b/src/app.test.ts deleted file mode 100644 index 0682f2d..0000000 --- a/src/app.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import axios from 'axios'; - -describe('Server Status', () => { - it('The server is running', async () => { - let serverStatus; - try { - const response = await axios.get('http://localhost'); - serverStatus = response.status; - } catch (error) { - console.error(error); - } - - expect(serverStatus).toBe(200); - }) -}) \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index b11cfdb..17afecb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,7 @@ require('module-alias/register'); import { config } from 'dotenv'; import express from 'express'; +import path from 'path'; import toobusy from 'toobusy-js'; import compression from 'compression'; import helmet from 'helmet'; @@ -10,7 +11,6 @@ import * as error from "./middleware/error"; import writeRouter from '@src/controller/write'; import readRouter from '@src/controller/read'; import loginRouter from '@src/controller/login'; -import path from 'path'; import logger from '@src/scripts/logger'; import { baseRateLimiter, cleanup as cleanupRateLimitedIps } from './middleware/limit'; import { cleanupCSRF } from "@src/scripts/token"; diff --git a/src/controller/write.test.ts b/src/controller/write.test.ts deleted file mode 100644 index f447d22..0000000 --- a/src/controller/write.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import axios, { AxiosError } from 'axios'; - -describe('HEAD /write', () => { - it('with all parameters correctly set it should succeed', async () => { - const timestamp = new Date().getTime(); - const response = await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - expect(response.status).toBe(200); - }); - - it('without key it sends 403', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(403); - } - }); - - it('with user length not equal to 2 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=x&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - - it('with lat not between -90 and 90 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=91.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - it('with lon not between -180 and 180 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=181.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - it('with timestamp to old sends 422', async () => { - try { - const timestamp = new Date().getTime() - 24 * 60 * 60 * 1000 * 2; // two days ago - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }) - - it('with hdop not between 0 and 100 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - it('with altitude not between 0 and 10000 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - it('with speed not between 0 and 300 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); - - it('with heading not between 0 and 360 it sends 422', async () => { - try { - const timestamp = new Date().getTime(); - await axios.head(`http://localhost/write?user=xx&lat=45.000&lon=90.000×tamp=${timestamp}&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test`); - } catch (error) { - const axiosError = error as AxiosError; - expect(axiosError.response!.status).toBe(422); - } - }); -}); diff --git a/src/models/entry.test.ts b/src/models/entry.test.ts deleted file mode 100644 index bd2c0b4..0000000 --- a/src/models/entry.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { checkNumber, checkTime } from "./entry"; - - -describe("checkNumber", () => { - it("should throw error if value is not provided", () => { - expect(() => checkNumber(0, 100)("")).toThrow(new Error('is required')); - }); - - it("should throw error if value length is more than 12", () => { - expect(() => checkNumber(0, 100)("1234567890123")).toThrow(new Error('Should have a maximum of 11 digits')); - }); - - it("should throw error if value is not a number", () => { - expect(() => checkNumber(0, 100)("abc")).toThrow(new Error('Value should be between 0 and 100')); - }); - - it("should return true if value is a valid number within range", () => { - expect(checkNumber(0, 100)("50")).toBe(true); - }); -}); - -describe("checkTime", () => { - it("should throw error if value is not a number", () => { - expect(() => checkTime("abc")).toThrow(new Error('Timestamp should be a number')); - }); - - it("should throw error if value is not a valid date", () => { - expect(() => checkTime("99999999999999999999")).toThrow(new Error('Timestamp should represent a valid date')); - }); - - it("should throw error if value is more than 1 day in the past", () => { - const date = new Date(); - date.setDate(date.getDate() - 2); // Set date to 2 days ago - expect(() => checkTime(date.getTime().toString())).toThrow(new Error('Timestamp should represent a date not further from server time than 1 day')); - }); - - it("should throw error if value is more than 1 day in the future", () => { - const date = new Date(); - date.setDate(date.getDate() + 2); // Set date to 2 days in the future - expect(() => checkTime(date.getTime().toString())).toThrow(new Error('Timestamp should represent a date not further from server time than 1 day')); - }); - - it("should return true if value is a valid timestamp within 1 day", () => { - const date = new Date(); - expect(checkTime(date.getTime().toString())).toBe(true); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 613bc08..5f168cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "target": "ES6", "sourceMap": true, "baseUrl": "./src", + "esModuleInterop": true, "paths": { "@src/*": ["./*"], } From 16429e670fb6e65fbc5d6117d8f266304c4f2af8 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 3 Apr 2024 15:26:57 +0200 Subject: [PATCH 063/206] [Task] npm upgrade --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2953ac7..2c7dff3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", "concurrently": "^8.2.2", - "dotenv": "^16.3.1", + "dotenv": "^16.4.5", "eslint": "^8.56.0", "eslint-plugin-jest": "^27.6.3", "install": "^0.13.0", @@ -3118,15 +3118,15 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "dev": true, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/ecdsa-sig-formatter": { diff --git a/package.json b/package.json index 4e3216f..009c4dd 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", "concurrently": "^8.2.2", - "dotenv": "^16.3.1", + "dotenv": "^16.4.5", "eslint": "^8.56.0", "eslint-plugin-jest": "^27.6.3", "install": "^0.13.0", From d65da31521e19b8766447ff3c0e69ec674421ad8 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 5 Apr 2024 13:36:34 +0200 Subject: [PATCH 064/206] 58 react setup (#59) * [Task] #58 install react via npm, incl. types and eslint plugins * [Task] #58, tsconfig for react folder * [Task] #58 esLint config * [Task] #58, webpack and react setup * [Task] #58, render welcome from express instead of static * [Task] #58 eslint scripts * [Task] #58, eslint react setup * [TASK] #58 integrate webpack in build and dev npm scripts --- .eslintrc.json | 4 +- .github/workflows/eslint.yml | 2 + .vscode/settings.json | 3 +- httpdocs/index.html | 57 - httpdocs/js/login.js | 1 - package-lock.json | 2677 +++++++++++++++++++++++++++++++++- package.json | 19 +- src/app.ts | 2 +- src/cache.ts | 15 - src/client/.eslintrc.json | 30 + src/client/index.tsx | 17 + src/client/tsconfig.json | 10 + src/error.ts | 32 - tsconfig.json | 2 +- views/index.ejs | 69 + webpack.config.js | 29 + 16 files changed, 2782 insertions(+), 187 deletions(-) delete mode 100644 httpdocs/index.html delete mode 100644 src/cache.ts create mode 100644 src/client/.eslintrc.json create mode 100644 src/client/index.tsx create mode 100644 src/client/tsconfig.json delete mode 100644 src/error.ts create mode 100644 views/index.ejs create mode 100644 webpack.config.js diff --git a/.eslintrc.json b/.eslintrc.json index aa4c83b..f09bcb4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,7 +2,7 @@ "root": true, "parser": "@typescript-eslint/parser", "parserOptions": { - "ecmaVersion": 2024, + "ecmaVersion": "latest", "sourceType": "module", "project": "tsconfig.json" }, @@ -22,6 +22,6 @@ //"@typescript-eslint/no-unused-vars": "warn" "jest/no-conditional-expect": "off" }, - "ignorePatterns": ["dist", "jest.config.js", "httpdocs"] + "ignorePatterns": ["dist", "jest.config.js", "httpdocs", "webpack.config.js", "src/client"] } diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 95578d7..d8e79a7 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -19,3 +19,5 @@ jobs: run: npx eslint src/ --fix - name: Lint client-side code run: npx eslint httpdocs/js/ --fix + - name: Lint react code + run: npx eslint src/client/ --fix diff --git a/.vscode/settings.json b/.vscode/settings.json index 700d626..4a44c2b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "workbench.editor.enablePreview": false, - "editor.rename.enablePreview": false + "editor.rename.enablePreview": false, + "typescript.tsdk": "node_modules\\typescript\\lib" } \ No newline at end of file diff --git a/httpdocs/index.html b/httpdocs/index.html deleted file mode 100644 index 99843e4..0000000 --- a/httpdocs/index.html +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - Welcome Page - - - -

Welcome

- - diff --git a/httpdocs/js/login.js b/httpdocs/js/login.js index 8b13789..e69de29 100644 --- a/httpdocs/js/login.js +++ b/httpdocs/js/login.js @@ -1 +0,0 @@ - diff --git a/package-lock.json b/package-lock.json index 2c7dff3..53f3e30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,8 @@ "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", "module-alias": "^2.2.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", "toobusy-js": "^0.5.1" }, "devDependencies": { @@ -32,6 +34,8 @@ "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.11.30", + "@types/react": "^18.2.74", + "@types/react-dom": "^18.2.24", "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", @@ -39,14 +43,21 @@ "concurrently": "^8.2.2", "dotenv": "^16.4.5", "eslint": "^8.56.0", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^27.6.3", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^4.6.0", "install": "^0.13.0", "jest": "^29.7.0", "nodemon": "^3.0.2", "npm": "^10.5.0", "ts-jest": "^29.1.2", + "ts-loader": "^9.5.1", "ts-node": "^10.9.2", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -773,6 +784,15 @@ "node": ">=12" } }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1341,19 +1361,29 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -1364,14 +1394,34 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -1610,6 +1660,32 @@ "@types/node": "*" } }, + "node_modules/@types/eslint": { + "version": "8.56.7", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz", + "integrity": "sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -1698,6 +1774,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, "node_modules/@types/jsonwebtoken": { "version": "9.0.6", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", @@ -1722,6 +1804,12 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true + }, "node_modules/@types/qs": { "version": "6.9.11", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", @@ -1734,6 +1822,25 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, + "node_modules/@types/react": { + "version": "18.2.74", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.74.tgz", + "integrity": "sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.24", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.24.tgz", + "integrity": "sha512-cN6upcKd8zkGy4HU9F1+/s98Hrp6D4MOcippK4PoE8OZRngohHZpbJn1GsaDLz87MqvHNoT13nHvNqM9ocRHZg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", @@ -2100,6 +2207,208 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2129,6 +2438,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -2195,6 +2513,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2286,11 +2613,56 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -2300,6 +2672,135 @@ "node": ">=8" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.toreversed": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", + "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", + "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.1.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true + }, "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", @@ -2311,6 +2812,30 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", + "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/axios": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", @@ -2322,6 +2847,15 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2690,9 +3224,18 @@ "node": ">=10" } }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ @@ -2725,6 +3268,20 @@ "node": ">=12" } }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2765,6 +3322,12 @@ "color-support": "bin.js" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2777,6 +3340,12 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -2958,6 +3527,69 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -3027,6 +3659,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3049,6 +3698,15 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -3187,6 +3845,31 @@ "node": ">= 0.8" } }, + "node_modules/enhanced-resolve": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", + "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3196,6 +3879,66 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -3215,6 +3958,89 @@ "node": ">= 0.4" } }, + "node_modules/es-iterator-helpers": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz", + "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", + "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==", + "dev": true + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3296,6 +4122,131 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/eslint-module-utils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils/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==", + "dev": true + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/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==", + "dev": true + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-jest": { "version": "27.6.3", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.3.tgz", @@ -3466,35 +4417,153 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", + "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", "dev": true, "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "@babel/runtime": "^7.23.2", + "aria-query": "^5.3.0", + "array-includes": "^3.1.7", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "=4.7.0", + "axobject-query": "^3.2.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.15", + "hasown": "^2.0.0", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=4.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, + "node_modules/eslint-plugin-jsx-a11y/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==", + "dev": true + }, + "node_modules/eslint-plugin-react": { + "version": "7.34.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz", + "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlast": "^1.2.4", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.toreversed": "^1.1.2", + "array.prototype.tosorted": "^1.1.3", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.17", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7", + "object.hasown": "^1.1.3", + "object.values": "^1.1.7", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.10" + }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=4" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "node_modules/eslint/node_modules/debug": { + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", @@ -3609,6 +4678,15 @@ "node": ">= 0.6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3772,6 +4850,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", @@ -3874,6 +4961,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -3914,6 +5010,15 @@ } } }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -3993,6 +5098,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gauge": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", @@ -4069,6 +5201,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4100,6 +5249,12 @@ "node": ">= 6" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -4115,6 +5270,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -4158,6 +5328,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -4200,15 +5379,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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==" }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "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" }, @@ -4392,6 +5586,29 @@ "node": ">= 0.10" } }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4400,12 +5617,55 @@ "node": ">= 0.10" } }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4418,6 +5678,34 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -4430,6 +5718,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4439,6 +5757,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "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", @@ -4456,19 +5786,58 @@ "node": ">=6" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", @@ -4477,6 +5846,21 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -4486,6 +5870,61 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -4498,12 +5937,112 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -4623,6 +6162,19 @@ "node": ">=8" } }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, "node_modules/jake": { "version": "10.8.7", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", @@ -5207,8 +6759,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -5296,6 +6847,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -5324,6 +6890,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -5333,6 +6908,24 @@ "node": ">=6" } }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5361,6 +6954,15 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5428,6 +7030,17 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -5568,6 +7181,15 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -5634,6 +7256,12 @@ "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==", + "dev": true + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -8549,51 +10177,158 @@ "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" - }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" } }, - "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==", + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, "dependencies": { - "wrappy": "1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "dependencies": { - "mimic-fn": "^2.1.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=6" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.hasown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", + "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "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==", + "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/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { @@ -8822,6 +10557,15 @@ "node": ">=8" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8870,6 +10614,23 @@ "node": ">= 6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -8953,6 +10714,15 @@ } ] }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -8975,6 +10745,29 @@ "node": ">= 0.8" } }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -9006,12 +10799,63 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9139,6 +10983,24 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -9158,11 +11020,54 @@ } ] }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "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/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -9205,6 +11110,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -9240,11 +11154,38 @@ "node": ">= 0.4" } }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "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/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9418,6 +11359,81 @@ "node": ">=8" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -9483,6 +11499,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tar": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", @@ -9499,8 +11524,118 @@ "node": ">=10" } }, - "node_modules/test-exclude": { - "version": "6.0.0", + "node_modules/terser": { + "version": "5.30.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz", + "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/terser-webpack-plugin/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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, @@ -9643,6 +11778,35 @@ } } }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -9686,6 +11850,39 @@ } } }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -9752,6 +11949,79 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -9765,6 +12035,21 @@ "node": ">=14.17" } }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -9892,11 +12177,170 @@ "makeerror": "1.0.12" } }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/webpack": { + "version": "5.91.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", + "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.16.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -9921,6 +12365,85 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -9929,6 +12452,12 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 009c4dd..e13dc98 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,17 @@ "main": "index.js", "scripts": { "prebuild": "rm -rf dist/*", - "build": "npx tsc", - "build:prod": "npx tsc -p ./tsconfig.prod.json", + "build": "tsc && webpack", + "build:prod": "tsc -p ./tsconfig.prod.json && webpack --mode production", "postbuild": "cp -R httpdocs/ dist/", "start": "node dist/app.js", "dev": "npm run prebuild && npm run postbuild && concurrently \"npm:dev:*\"", "dev:ts": "nodemon --config nodemon-ts.json", "dev:static": "nodemon --config nodemon-static.json", + "dev:webpack": "webpack --watch", "lint": "eslint . --fix", "lint:client": "eslint httpdocs/js/ --fix", + "lint:react": "eslint src/client/ --fix", "test": "jest", "test:app": "jest src/tests/app.test.ts", "test:login": "jest src/tests/login.test.ts", @@ -32,6 +34,8 @@ "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.11.30", + "@types/react": "^18.2.74", + "@types/react-dom": "^18.2.24", "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", @@ -39,14 +43,21 @@ "concurrently": "^8.2.2", "dotenv": "^16.4.5", "eslint": "^8.56.0", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^27.6.3", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^4.6.0", "install": "^0.13.0", "jest": "^29.7.0", "nodemon": "^3.0.2", "npm": "^10.5.0", "ts-jest": "^29.1.2", + "ts-loader": "^9.5.1", "ts-node": "^10.9.2", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4" }, "dependencies": { "bcrypt": "^5.1.1", @@ -61,6 +72,8 @@ "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", "module-alias": "^2.2.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", "toobusy-js": "^0.5.1" }, "_moduleAliases": { diff --git a/src/app.ts b/src/app.ts index 17afecb..f7e3eab 100644 --- a/src/app.ts +++ b/src/app.ts @@ -54,7 +54,7 @@ app.use((req, res, next) => { // limit body for specific http methods // routes app.get('/', (req, res) => { logger.log(req.ip + " - " + res.locals.ip, true); - res.send('Hello World, via TypeScript and Node.js! ' + `ENV: ${process.env.NODE_ENV}`); + res.render("index"); }); app.use('/write', writeRouter); diff --git a/src/cache.ts b/src/cache.ts deleted file mode 100644 index c64ae4a..0000000 --- a/src/cache.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Request, Response, NextFunction } from 'express'; - -const setCache = function (req: Request, res: Response, next: NextFunction) { - const seconds = 60 * 5; // 5 minuits - - // cache get requests but nothing else - if (req.method == "GET") { - res.set("Cache-control", `public, max-age=${seconds}`); - } else { - res.set("Cache-control", 'no-store'); - } - - next(); -} -export default setCache; \ No newline at end of file diff --git a/src/client/.eslintrc.json b/src/client/.eslintrc.json new file mode 100644 index 0000000..65c7867 --- /dev/null +++ b/src/client/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "root": true, + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "plugin:react/recommended", + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "react", + "@typescript-eslint" + ], + "settings": { + "react": { + "version": "detect" + } + }, + "rules": {} +} \ No newline at end of file diff --git a/src/client/index.tsx b/src/client/index.tsx new file mode 100644 index 0000000..1dea49e --- /dev/null +++ b/src/client/index.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { Root, createRoot } from 'react-dom/client'; + +const App = () => { + return ( +

Hello, React!

+ ); +}; + +const container = document.getElementById('root'); +let root:Root; +if (container) { + root = createRoot(container); + root.render(); +} else { + console.error("root not found"); +} diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json new file mode 100644 index 0000000..c0455c1 --- /dev/null +++ b/src/client/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": ["dom","ES6"], + "outDir": "../../dist/httpdocs", + "rootDir": "../../", + "jsx": "react", + }, + "include": ["**/*.tsx", "**/*.ts"], +} \ No newline at end of file diff --git a/src/error.ts b/src/error.ts deleted file mode 100644 index 0b0bc04..0000000 --- a/src/error.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Request, Response, NextFunction } from "express"; - -export function notFound(req: Request, res: Response, next: NextFunction) { - res.status(404); - const error = new Error(`🔍 - Not Found - ${req.originalUrl}`); - next(error); -} - -export function handler(err: Error, req: Request, res: Response, next: NextFunction) { - const statusCode = res.statusCode !== 200 ? res.statusCode : 500; - res.status(statusCode); - - let message; - try { - const jsonMessage = JSON.parse(err.message); - message = jsonMessage; - } catch (e) { - message = err.message; - } - - const responseBody = { - status: statusCode, - name: err.name, - message: message, - stack: process.env.NODE_ENV === "development" ? err.stack : "---" - }; - - //logger.error(responseBody); - res.json(responseBody); - - next(); -} diff --git a/tsconfig.json b/tsconfig.json index 5f168cd..64098b6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@tsconfig/node20/tsconfig.json", "include": ["src/**/*"], - "exclude": ["node_modules"], + "exclude": ["node_modules", "src/client/"], "compilerOptions": { "rootDir": "src", "outDir": "dist", diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..7cce0f1 --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,69 @@ + + + + + + + Welcome Page + + + + +
+

Welcome

+
+ + + + \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..c082712 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,29 @@ +const path = require('path'); + +module.exports = (args) => { + const mode = args.mode || 'development'; + return { + entry: './src/client/index.tsx', + module: { + rules: [ + { + test: /\.tsx?$/, + use: { + loader: 'ts-loader', + options: { + configFile: 'src/client/tsconfig.json' + } + }, + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist/httpdocs/js'), + } + } +}; From 27ebfedf671ff284d441f2f9098fbc1cdf8661d5 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 9 Apr 2024 13:09:23 +0200 Subject: [PATCH 065/206] [Temp] Test csp --- src/client/components/App.tsx | 16 ++++++++++++++++ src/client/components/Contact.tsx | 15 +++++++++++++++ src/client/index.tsx | 7 +------ webpack.config.js | 1 + 4 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 src/client/components/App.tsx create mode 100644 src/client/components/Contact.tsx diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx new file mode 100644 index 0000000..b66becb --- /dev/null +++ b/src/client/components/App.tsx @@ -0,0 +1,16 @@ +import React, { Component } from 'react'; +import Contact from './Contact'; + +class App extends Component { + render() { + return ( +
+

Hello, React!

+ +
+ ); + } +} + + +export default App; \ No newline at end of file diff --git a/src/client/components/Contact.tsx b/src/client/components/Contact.tsx new file mode 100644 index 0000000..ad00ce7 --- /dev/null +++ b/src/client/components/Contact.tsx @@ -0,0 +1,15 @@ +import React, { Component } from 'react' + +export default class Contact extends Component { + render() { + return ( +
+

John Doe

+
    +
  • Email: jdoe@gmail.com
  • +
  • Phone: 555-555-5555
  • +
+
+ ) + } +} diff --git a/src/client/index.tsx b/src/client/index.tsx index 1dea49e..db63bdf 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,11 +1,6 @@ import * as React from 'react'; import { Root, createRoot } from 'react-dom/client'; - -const App = () => { - return ( -

Hello, React!

- ); -}; +import App from "./components/App"; const container = document.getElementById('root'); let root:Root; diff --git a/webpack.config.js b/webpack.config.js index c082712..f7641c1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,6 +3,7 @@ const path = require('path'); module.exports = (args) => { const mode = args.mode || 'development'; return { + mode: mode, entry: './src/client/index.tsx', module: { rules: [ From 0cc97762bebd1335605b72757e9d5edf294a54ef Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 9 Apr 2024 13:25:05 +0200 Subject: [PATCH 066/206] [FIX] Add views to be deployed to prod --- .github/workflows/ftp.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ftp.yml b/.github/workflows/ftp.yml index 7d20652..653f6fb 100644 --- a/.github/workflows/ftp.yml +++ b/.github/workflows/ftp.yml @@ -17,6 +17,9 @@ jobs: run: npm i - name: Build run: npm run build:prod + - name: copy views to be deployed + run: | + cp -R views/ dist/ - name: Upload ftp uses: airvzxf/ftp-deployment-action@latest with: From c05d21c5b14ac513ca11683e209e7fe42b7924b1 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 9 Apr 2024 13:59:01 +0200 Subject: [PATCH 067/206] [Task] disable csp for local development --- package.json | 2 +- src/app.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e13dc98..eab8ca7 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", "concurrently": "^8.2.2", - "dotenv": "^16.4.5", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^27.6.3", @@ -63,6 +62,7 @@ "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", + "dotenv": "^16.4.5", "ejs": "^3.1.9", "express": "^4.19.2", "express-rate-limit": "^7.2.0", diff --git a/src/app.ts b/src/app.ts index f7e3eab..bc4360c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -38,7 +38,9 @@ app.use((req, res, next) => { // clean up IPv6 Addresses } }) -app.use(helmet({ contentSecurityPolicy: { directives: { "default-src": "'self'", "img-src": "*" } } })); +if (process.env.NODE_ENV != "development") { + app.use(helmet({ contentSecurityPolicy: { directives: { "default-src": "'self'", "img-src": "*" } } })); +} app.use(cache); app.use(compression()) app.use(hpp()); From 7645937a78b75b84b47969266a488440e849d64a Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 15 Apr 2024 16:16:56 +0200 Subject: [PATCH 068/206] [Task] #58 base css including colors, deleted color classes in favor of variables --- httpdocs/css/base.css | 217 ++++++++++++++++++++++++++++++++++++++++ httpdocs/css/colors.css | 95 ------------------ 2 files changed, 217 insertions(+), 95 deletions(-) create mode 100644 httpdocs/css/base.css delete mode 100644 httpdocs/css/colors.css diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css new file mode 100644 index 0000000..b354071 --- /dev/null +++ b/httpdocs/css/base.css @@ -0,0 +1,217 @@ +@charset "UTF-8"; + +/* --------------------------------------------------------------------- +Project Name: LOREX +------------------------------------------------------------------------ +*1. Reset +*2. Global styles / Variables +*3. Helper styles +*4. Grid styles +*5. Media Queries +----------------------------------------------------------------------- */ + +/* ============================== + *1. Reset +================================= */ + +html { + font-size: 62.5%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + text-size-adjust: none; + scroll-behavior: smooth; +} +html, textarea, input, button { + font-family: sans-serif; + font-kerning: normal; +} +html, body { + height: 100%; +} + + +body, +p, h1, h2, h3, h4, h5, h6, +div, ul, ol, li, dd, dt, dl, table, td, th +blockquote, address, hr, pre, +article, aside, audio, canvas, details, figure, figcaption, footer, header, hgroup, +iframe, main, menu, nav, section, summary, video, +form, fieldset, legend, label, input, textarea { + margin: 0; padding: 0; + box-sizing: border-box; +} +::before, ::after { box-sizing: border-box; } + + +p, li, h1, h2, h3, h4, h5, h6 { + font-weight: normal; + font-size: 1em; + -webkit-text-size-adjust: none; + -ms-text-size-adjust: none; + text-size-adjust: none; + /* -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing : grayscale; */ + + hyphens: auto; + -webkit-hyphenate-limit-lines: 1; + hyphenate-limit-lines: 1; /* consecutive */ + hyphenate-limit-chars: 6 3 3; +} + +a {background-color: inherit;} +a:is(:hover, :active) {text-decoration: underline;} +a:visited {color: #509; /* background-color: #eee; */} +a:active, button:active {outline: none;} + +:focus {outline: 0.1em dotted; outline-offset: 0.1em;} +:focus:not(:focus-visible) { outline: none; } +embed:focus, object:focus, a img {border: none;} + +img, object, embed {display: inline-block; max-width: 100%; vertical-align: baseline;} + +img:-moz-broken, img:-moz-user-disabled {display: none;} + +abbr[title], dfn[title], q {cursor: help; border-bottom: 0.1em dotted;} +input[disabled], textarea[disabled], button[disabled] {cursor: not-allowed;} + +button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; padding: 0; +} + +label[for], button, select, summary, [type=radio], [type=submit], [type=checkbox] { + cursor: pointer; +} + +/* touch devices, and anything other where there is no mouse */ +@media screen and (pointer: coarse) { + label[for] { font-size: 1.05em; } + [type="checkbox"] { width: 1.5rem; height: 1.5rem; } + button { min-height: 3rem; } +} + +/* ============================== + *2. Global styles / Variables +================================= */ +/* +created by atmos https://app.atmos.style/65cc9eaec76d443c0a796d4b + +** base configuration colors ** +Main: #f90 +Info: #231aee +Danger: #ff0000 +Success: #59ec04 +Neutral: #131211 +*/ +:root { + --main: var(--main-L6); + --main-L1: oklch(10% 0.02 64.55); + --main-L2: oklch(25% 0.056 64.55); + --main-L3: oklch(37.5% 0.085 64.55); + --main-L4: oklch(50% 0.114 64.55); + --main-L5: oklch(62.5% 0.142 64.55); + --main-L6: oklch(77.2% 0.1738 64.55); /* base */ + --main-L7: oklch(90% 0.06 64.55); + + --info: var(--info-L4); + --info-L1: oklch(10% 0.055 268.01); + --info-L2: oklch(25% 0.158 268.01); + --info-L3: oklch(37.5% 0.237 268.01); + --info-L4: oklch(50% 0.2838 268.01); /* base */ + --info-L5: oklch(62.5% 0.19 268.01); + --info-L6: oklch(77.2% 0.109 268.01); + --info-L7: oklch(90% 0.04 268.01); + + --alert: var(--alert-L5); + --alert-L1: oklch(10% 0.036 29.23); + --alert-L2: oklch(25% 0.103 29.23); + --alert-L3: oklch(37.5% 0.154 29.23); + --alert-L4: oklch(50% 0.195 29.23); + --alert-L5: oklch(62.5% 0.2577 29.23); /* base */ + --alert-L6: oklch(77.2% 0.133 29.23); + --alert-L7: oklch(90% 0.045 29.23); + + --success: var(--success-L6); + --success-L1: oklch(10% 0.029 138.96); + --success-L2: oklch(25% 0.083 138.96); + --success-L3: oklch(37.5% 0.124 138.96); + --success-L4: oklch(50% 0.157 138.96); + --success-L5: oklch(62.5% 0.208 138.96); + --success-L6: oklch(77.2% 0.2607 138.96); /* base */ + --success-L7: oklch(90% 0.201 138.96); + + --neutral: var(--neutral-L2); + --neutral-L1: oklch(10% 0.001 67.66); + --neutral-L2: oklch(25% 0.0026 67.66); /* base */ + --neutral-L3: oklch(37.5% 0.006 67.66); + --neutral-L4: oklch(50% 0.007 67.66); + --neutral-L5: oklch(62.5% 0.009 67.66); + --neutral-L6: oklch(77.2% 0.011 67.66); + --neutral-L7: oklch(90% 0.004 67.66); +} + + +/* ============================== + *3. Helper styles +================================= */ + +/* visually hidden */ +.hideText { + text-indent: 100%; + white-space: nowrap; + overflow: hidden; +} + +@media screen and + (prefers-reduced-motion: reduce), + (update: slow) { + :root { + scroll-behavior: auto; + } + * { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + } +} + +/* development */ +#html:target::before, #html:target::after { + content: ""; + font-size: 200%; + position: fixed; top: 1em; left: 1em; + width: 10em; + padding: 0.5em; + opacity: 0.8; + + border: 0.2em solid red; + background: rgba(255, 50, 50, 0.6); + font-weight: bold; + text-align: center; + text-shadow: 0.1em 0.1em 0.2em #fff; + z-index: 100; +} +#html:target::after { + border-color: blue; + background: rgba(50, 50, 255, 0.6); + left: 14em; +} + + + +/* ============================== + *4. Grid styles +================================= */ + +#react-root { + display: contents; +} + +/* ============================== + *5. Media Queries +================================= */ + +@media (min-width: 30em){#html:target::before {content: ">= 480px"; }} +@media (min-width: 48em){#html:target::before {content: ">= 768px"; }} +@media (min-width: 64em){#html:target::before {content: ">= 1024px"; }} +@media (min-width: 75em){#html:target::before {content: ">= 1200px"; }} +@media (min-width: 100em){#html:target::before {content: ">= 1600px"; }} \ No newline at end of file diff --git a/httpdocs/css/colors.css b/httpdocs/css/colors.css deleted file mode 100644 index d77a7fd..0000000 --- a/httpdocs/css/colors.css +++ /dev/null @@ -1,95 +0,0 @@ -/* -created by atmos https://app.atmos.style/65cc9eaec76d443c0a796d4b - -** base configuration colors ** -Main: #f90 -Info: #231aee -Danger: #ff0000 -Success: #59ec04 -Neutral: #131211 -*/ - -[class*=color] { - --lightness: 67.66%; - --hue: 64.55; - --chroma: 0.007; - color: oklch(var(--lightness) var(--chroma) var(--hue)); - - &[class*=l1] {--lightness: 10%;} - &[class*=l2] {--lightness: 25%;} - &[class*=l3] {--lightness: 37.5%;} - &[class*=l4] {--lightness: 50%;} - &[class*=l5] {--lightness: 62.5%;} - &[class*=l6] {--lightness: 77.2%;} - &[class*=l7] {--lightness: 90%;} - - &[class*=main] { - --lightness: 77.2%; - --chroma: 0.1738; - --hue: 64.55; - - &[class*=l1] {--chroma: 0.02;} - &[class*=l2] {--chroma: 0.056;} - &[class*=l3] {--chroma: 0.085;} - &[class*=l4] {--chroma: 0.114;} - &[class*=l5] {--chroma: 0.142;} - &[class*=l6] {--chroma: 0.1738;} /* base */ - &[class*=l7] {--chroma: 0.06;} - } - - &[class*=info] { - --lightness: 44.87%; - --chroma: 0.2838; - --hue: 268.0; - - &[class*=l1] {--chroma: 0.055;} - &[class*=l2] {--chroma: 0.158;} - &[class*=l3] {--chroma: 0.237;} - &[class*=l4] {--chroma: 0.2838;} /* base */ - &[class*=l5] {--chroma: 0.19;} - &[class*=l6] {--chroma: 0.109;} - &[class*=l7] {--chroma: 0.04;} - } - - &[class*=alert] { - --lightness: 62.8%; - --chroma: 0.2577; - --hue: 29.23; - - &[class*=l1] {--chroma: 0.036;} - &[class*=l2] {--chroma: 0.103;} - &[class*=l3] {--chroma: 0.154;} - &[class*=l4] {--chroma: 0.195;} - &[class*=l5] {--chroma: 0.2577;} /* base */ - &[class*=l6] {--chroma: 0.133;} - &[class*=l7] {--chroma: 0.045;} - } - - &[class*=success] { - --lightness: 83%; - --chroma: 0.2607; - --hue: 138.96; - - &[class*=l1] {--chroma: 0.029;} - &[class*=l2] {--chroma: 0.083;} - &[class*=l3] {--chroma: 0.124;} - &[class*=l4] {--chroma: 0.157;} - &[class*=l5] {--chroma: 0.208;} - &[class*=l6] {--chroma: 0.2607;} /* base */ - &[class*=l7] {--chroma: 0.201;} - } - - &[class*=neutral] { - --lightness: 18.3%; - --chroma: 0.0026; - --hue: 67.66; - - &[class*=l1] {--chroma: 0.001;} - &[class*=l2] {--chroma: 0.0026;} /* base */ - &[class*=l3] {--chroma: 0.006;} - &[class*=l4] {--chroma: 0.007;} - &[class*=l5] {--chroma: 0.009;} - &[class*=l6] {--chroma: 0.011;} - &[class*=l7] {--chroma: 0.004;} - } -} From 265ce0c1dff18210bfff932ca3bfe683c361f099 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 15 Apr 2024 16:17:30 +0200 Subject: [PATCH 069/206] [Task] #58 typescript setup for react --- src/client/index.tsx | 2 +- src/client/tsconfig.json | 4 ++-- src/client/types.d.ts | 11 +++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 src/client/types.d.ts diff --git a/src/client/index.tsx b/src/client/index.tsx index db63bdf..d542e5f 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Root, createRoot } from 'react-dom/client'; import App from "./components/App"; -const container = document.getElementById('root'); +const container = document.getElementById('react-root'); let root:Root; if (container) { root = createRoot(container); diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index c0455c1..04b7ddc 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -1,10 +1,10 @@ { - "extends": "../../tsconfig.json", "compilerOptions": { "lib": ["dom","ES6"], "outDir": "../../dist/httpdocs", "rootDir": "../../", "jsx": "react", + "esModuleInterop": true }, - "include": ["**/*.tsx", "**/*.ts"], + "include": ["**/*.tsx", "**/*.ts", "types.d.ts"] } \ No newline at end of file diff --git a/src/client/types.d.ts b/src/client/types.d.ts new file mode 100644 index 0000000..6b2cf3f --- /dev/null +++ b/src/client/types.d.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +declare module "*.module.css"; + +declare namespace client { + interface contact { + name: string, + email: string, + phone: string + hobby?: string + } +} \ No newline at end of file From adfde74e4557348305a84cb0060351f924bacb88 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 15 Apr 2024 16:18:02 +0200 Subject: [PATCH 070/206] [Task] #58 webpack setup for react and typescript --- package-lock.json | 213 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 + webpack.config.js | 12 +++ 3 files changed, 225 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53f3e30..ddd6690 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", + "dotenv": "^16.4.5", "ejs": "^3.1.9", "express": "^4.19.2", "express-rate-limit": "^7.2.0", @@ -41,7 +42,7 @@ "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", "concurrently": "^8.2.2", - "dotenv": "^16.4.5", + "css-loader": "^7.1.0", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^27.6.3", @@ -52,6 +53,7 @@ "jest": "^29.7.0", "nodemon": "^3.0.2", "npm": "^10.5.0", + "style-loader": "^4.0.0", "ts-jest": "^29.1.2", "ts-loader": "^9.5.1", "ts-node": "^10.9.2", @@ -3527,6 +3529,53 @@ "node": ">= 8" } }, + "node_modules/css-loader": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.0.tgz", + "integrity": "sha512-VFNj47MAG84MqYDdh9puJG0h98Xs7gEYaX0aeGkfjYqBLB0seOE325sVbqWwaNu3hMZwEP4bB+F4gvF+A63qMA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3779,7 +3828,6 @@ "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true, "engines": { "node": ">=12" }, @@ -5504,6 +5552,18 @@ "node": ">=0.10.0" } }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -7242,6 +7302,24 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -10566,6 +10644,112 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11274,6 +11458,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -11475,6 +11668,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "dev": true, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/package.json b/package.json index eab8ca7..9e5607c 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", "concurrently": "^8.2.2", + "css-loader": "^7.1.0", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^27.6.3", @@ -51,6 +52,7 @@ "jest": "^29.7.0", "nodemon": "^3.0.2", "npm": "^10.5.0", + "style-loader": "^4.0.0", "ts-jest": "^29.1.2", "ts-loader": "^9.5.1", "ts-node": "^10.9.2", diff --git a/webpack.config.js b/webpack.config.js index f7641c1..18b4969 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -17,6 +17,18 @@ module.exports = (args) => { }, exclude: /node_modules/, }, + { + test: /\.css$/, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + modules: true, + }, + }, + ], + } ], }, resolve: { From 9908b73bb2be2217c4880bb47bba99b107e4dbe1 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 15 Apr 2024 16:18:43 +0200 Subject: [PATCH 071/206] [Task] #58 app setup react --- src/app.ts | 2 +- src/client/components/App.tsx | 7 +- src/client/components/Contact.tsx | 27 +++++-- src/client/components/css/app.module.css | 50 +++++++++++++ views/index.ejs | 93 ++++++++---------------- 5 files changed, 105 insertions(+), 74 deletions(-) create mode 100644 src/client/components/css/app.module.css diff --git a/src/app.ts b/src/app.ts index bc4360c..798e48a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -56,7 +56,7 @@ app.use((req, res, next) => { // limit body for specific http methods // routes app.get('/', (req, res) => { logger.log(req.ip + " - " + res.locals.ip, true); - res.render("index"); + res.render("index", {root: process.env.ROOT}); }); app.use('/write', writeRouter); diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index b66becb..4b413e2 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,12 +1,13 @@ import React, { Component } from 'react'; import Contact from './Contact'; +import * as css from"./css/app.module.css"; class App extends Component { render() { return ( -
-

Hello, React!

- +
+

Hello, React!

+
); } diff --git a/src/client/components/Contact.tsx b/src/client/components/Contact.tsx index ad00ce7..c7530f2 100644 --- a/src/client/components/Contact.tsx +++ b/src/client/components/Contact.tsx @@ -1,15 +1,26 @@ -import React, { Component } from 'react' +import React, { Component } from 'react'; +import * as classes from "./css/contact.module.css"; -export default class Contact extends Component { +export default class Contact extends Component { + static defaultProps = { + name: "no name", + email: "no email", + hobby: "no hobby" + } render() { + const {name, email, phone, hobby} = this.props; + return ( -
-

John Doe

-
    -
  • Email: jdoe@gmail.com
  • -
  • Phone: 555-555-5555
  • -
+
+

{name}

+
+
Email:
{email}
+
Phone:
{phone}
+
Hobby:
{hobby}
+
) } } + +Contact \ No newline at end of file diff --git a/src/client/components/css/app.module.css b/src/client/components/css/app.module.css new file mode 100644 index 0000000..081121e --- /dev/null +++ b/src/client/components/css/app.module.css @@ -0,0 +1,50 @@ +@keyframes slideInFromLeft { + 0% { + transform: translateX(-5%); + } + + 100% { + transform: translateX(5%); + } +} + +@keyframes move-it { + to { + background-position: 200px 0px; + } +} + +.app { + --bg1: var(--neutral-L7); + --bg2: var(--neutral-L7); + --text: var(--neutral-L1); + --shadow: var(--main); + @media (prefers-color-scheme: dark) { + --bg1: var(--neutral-L1); + --bg2: var(--neutral-L2); + --text: var(--main); + --shadow: var(--neutral-L1); + } + + height: 100%; + color: var(--text); + background: repeating-linear-gradient(45deg, + var(--bg1), + var(--bg1) 5%, + var(--bg2) 5%, + var(--bg2) 10%); + background-size: 200px 200px; + animation: move-it 10s linear infinite; + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + + + .headline { + text-align: center; + animation: 2s ease-in-out 0s infinite slideInFromLeft alternate-reverse, 3s ease-in-out 0s infinite textShadow; + text-shadow: 0 0 15px var(--shadow); + font-size: clamp(30px, 5dvmax, 100px); + } +} diff --git a/views/index.ejs b/views/index.ejs index 7cce0f1..84348d9 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -1,69 +1,38 @@ - - - - - Welcome Page - + + + + LOREX - Osmand Webtracking Frontend + + + + + + + + + + + + + + + + + + + + + -
-

Welcome

-
+
+

Welcome

+
+ - - - \ No newline at end of file + \ No newline at end of file From 0d72aeb8d0887e2fb66b43187a51817e216837d5 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 15 Apr 2024 16:19:42 +0200 Subject: [PATCH 072/206] [Temp] #58 conctact module css --- src/client/components/css/contact.module.css | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/client/components/css/contact.module.css diff --git a/src/client/components/css/contact.module.css b/src/client/components/css/contact.module.css new file mode 100644 index 0000000..6e76527 --- /dev/null +++ b/src/client/components/css/contact.module.css @@ -0,0 +1,23 @@ +.contact { + h4 { + text-align: center; + } + + dl { + margin-inline: 0; + list-style: none; + display: grid; + grid-template-columns: min-content min-content; + justify-content: center; + } + + dt { + grid-column: 1; + text-align: left; + } + dd { + grid-column: 2; + text-align: center; + } +} + From b63bb97045a9443e11ca9d8658f1e7faecf96e3b Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 15 Apr 2024 16:22:08 +0200 Subject: [PATCH 073/206] [Task] #58 remove learning files --- src/client/components/App.tsx | 2 -- src/client/components/Contact.tsx | 26 -------------------- src/client/components/css/contact.module.css | 23 ----------------- 3 files changed, 51 deletions(-) delete mode 100644 src/client/components/Contact.tsx delete mode 100644 src/client/components/css/contact.module.css diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 4b413e2..8e1f934 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import Contact from './Contact'; import * as css from"./css/app.module.css"; class App extends Component { @@ -7,7 +6,6 @@ class App extends Component { return (

Hello, React!

-
); } diff --git a/src/client/components/Contact.tsx b/src/client/components/Contact.tsx deleted file mode 100644 index c7530f2..0000000 --- a/src/client/components/Contact.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { Component } from 'react'; -import * as classes from "./css/contact.module.css"; - -export default class Contact extends Component { - static defaultProps = { - name: "no name", - email: "no email", - hobby: "no hobby" - } - render() { - const {name, email, phone, hobby} = this.props; - - return ( -
-

{name}

-
-
Email:
{email}
-
Phone:
{phone}
-
Hobby:
{hobby}
-
-
- ) - } -} - -Contact \ No newline at end of file diff --git a/src/client/components/css/contact.module.css b/src/client/components/css/contact.module.css deleted file mode 100644 index 6e76527..0000000 --- a/src/client/components/css/contact.module.css +++ /dev/null @@ -1,23 +0,0 @@ -.contact { - h4 { - text-align: center; - } - - dl { - margin-inline: 0; - list-style: none; - display: grid; - grid-template-columns: min-content min-content; - justify-content: center; - } - - dt { - grid-column: 1; - text-align: left; - } - dd { - grid-column: 2; - text-align: center; - } -} - From 2d6bab032e93a2eb6d600d6fa75febc8c45077c3 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 18 Apr 2024 17:45:21 +0200 Subject: [PATCH 074/206] [Task] #61, create font --- httpdocs/css/base.css | 15 +++- httpdocs/font/OFL.txt | 95 +++++++++++++++++++++++ httpdocs/font/science-gothic.woff2 | Bin 0 -> 85308 bytes src/client/components/css/app.module.css | 20 +---- views/index.ejs | 1 - 5 files changed, 110 insertions(+), 21 deletions(-) create mode 100644 httpdocs/font/OFL.txt create mode 100644 httpdocs/font/science-gothic.woff2 diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index b354071..23af3a2 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -17,12 +17,12 @@ Project Name: LOREX html { font-size: 62.5%; -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; - text-size-adjust: none; + -ms-text-size-adjust: 100%; + text-size-adjust: none; scroll-behavior: smooth; } html, textarea, input, button { - font-family: sans-serif; + font-family: Science-Gothic, sans-serif; font-kerning: normal; } html, body { @@ -92,6 +92,15 @@ label[for], button, select, summary, [type=radio], [type=submit], [type=checkbox /* ============================== *2. Global styles / Variables ================================= */ + +@font-face { + font-family: 'Science-Gothic'; + src: url('/font/science-gothic.woff2') format('woff2'); /* variable font, no tech or variations here, due to browsers support and no fallback font anyway */ + font-weight: 100 900; + font-stretch: 50% 200%; + unicode-range: U+0020-007E, U+2026; +} + /* created by atmos https://app.atmos.style/65cc9eaec76d443c0a796d4b diff --git a/httpdocs/font/OFL.txt b/httpdocs/font/OFL.txt new file mode 100644 index 0000000..737571f --- /dev/null +++ b/httpdocs/font/OFL.txt @@ -0,0 +1,95 @@ +Copyright © 2019–2024 Font Detective LLC + +This Font Software is licensed under the SIL Open Font License, Version 1.1, +with no Reserved Font Name. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org/ + +ADDITIONAL non-font source files are licensed to others under the Apache 2.0 open source license (https://www.apache.org/licenses/LICENSE-2.0). + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/httpdocs/font/science-gothic.woff2 b/httpdocs/font/science-gothic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..948e930bd75a98c4e328d972ca297b9c6e28d4b5 GIT binary patch literal 85308 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πafojw%G_C= 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

GD;u$KKkjJkL{*;U z&(aSN-R%XzkRY704Z-;gZG@26*g#ezh|e^(7ZC2q`*Lj~M%p9E%EC!UF0Bhffgm(* z5X)>tN028my;$qFP}q=|hcUb{naf2If^sbTibQy{`BKK_QyE{brno#%HfC^VIg+H( zOja39&+Lt?DlC|n#j+V9pKJK1IFk40<_(nm9y5yz6c<8#AVV^%Sq}mkAZDltlC;w9U++46M?G7?^?|WvHa| zBt6~Ga;0X - From 12f2a6f53ab8777bcfffdd8cebda0b3270d3fe63 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 18 Apr 2024 17:49:25 +0200 Subject: [PATCH 075/206] Revert "[Task] #58 remove learning files" This reverts commit b63bb97045a9443e11ca9d8658f1e7faecf96e3b. --- src/client/components/App.tsx | 2 ++ src/client/components/Contact.tsx | 26 ++++++++++++++++++++ src/client/components/css/contact.module.css | 23 +++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 src/client/components/Contact.tsx create mode 100644 src/client/components/css/contact.module.css diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 8e1f934..4b413e2 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,4 +1,5 @@ import React, { Component } from 'react'; +import Contact from './Contact'; import * as css from"./css/app.module.css"; class App extends Component { @@ -6,6 +7,7 @@ class App extends Component { return (

Hello, React!

+
); } diff --git a/src/client/components/Contact.tsx b/src/client/components/Contact.tsx new file mode 100644 index 0000000..c7530f2 --- /dev/null +++ b/src/client/components/Contact.tsx @@ -0,0 +1,26 @@ +import React, { Component } from 'react'; +import * as classes from "./css/contact.module.css"; + +export default class Contact extends Component { + static defaultProps = { + name: "no name", + email: "no email", + hobby: "no hobby" + } + render() { + const {name, email, phone, hobby} = this.props; + + return ( +
+

{name}

+
+
Email:
{email}
+
Phone:
{phone}
+
Hobby:
{hobby}
+
+
+ ) + } +} + +Contact \ No newline at end of file diff --git a/src/client/components/css/contact.module.css b/src/client/components/css/contact.module.css new file mode 100644 index 0000000..6e76527 --- /dev/null +++ b/src/client/components/css/contact.module.css @@ -0,0 +1,23 @@ +.contact { + h4 { + text-align: center; + } + + dl { + margin-inline: 0; + list-style: none; + display: grid; + grid-template-columns: min-content min-content; + justify-content: center; + } + + dt { + grid-column: 1; + text-align: left; + } + dd { + grid-column: 2; + text-align: center; + } +} + From 887ecf5819571d8ee00ac54a73f4f6a46e533596 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Apr 2024 14:17:34 +0200 Subject: [PATCH 076/206] [Task] #61, adjust for darkmode --- httpdocs/css/base.css | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index 23af3a2..b5c67c0 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -20,6 +20,7 @@ html { -ms-text-size-adjust: 100%; text-size-adjust: none; scroll-behavior: smooth; + color-scheme: light dark; } html, textarea, input, button { font-family: Science-Gothic, sans-serif; @@ -75,7 +76,7 @@ abbr[title], dfn[title], q {cursor: help; border-bottom: 0.1em dotted;} input[disabled], textarea[disabled], button[disabled] {cursor: not-allowed;} button::-moz-focus-inner, input::-moz-focus-inner { - border: 0; padding: 0; + border: 0; padding: 0; } label[for], button, select, summary, [type=radio], [type=submit], [type=checkbox] { @@ -163,11 +164,15 @@ Neutral: #131211 *3. Helper styles ================================= */ -/* visually hidden */ -.hideText { - text-indent: 100%; - white-space: nowrap; - overflow: hidden; +.visually-hidden { + clip:rect(0 0 0 0); + border:0; + height:1px; + margin:-1px; + overflow:hidden; + padding:0; + position:absolute; + width:1px } @media screen and From aa0b6680b60ccb0efe1608b426c00768ac65e4cb Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Apr 2024 14:18:07 +0200 Subject: [PATCH 077/206] [Task] #61 apply base style to login --- httpdocs/css/login.css | 17 ++++++++++++----- views/login-form.ejs | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/httpdocs/css/login.css b/httpdocs/css/login.css index b9d1d7c..2980784 100644 --- a/httpdocs/css/login.css +++ b/httpdocs/css/login.css @@ -1,13 +1,20 @@ form { + font-size: 2rem; margin-inline: auto; - display: flex; - flex-wrap: wrap; - justify-content: space-between; max-width: 500px; - gap: 10px; +} +label { + display: block; + margin-block: 1em; } input, button { - flex-grow: 1; + height: 2em; + display: block; + margin-block: 0.25em 2em; + font-size: inherit; +} +button { + cursor: pointer; } textarea, h1 { flex-basis: 100%; diff --git a/views/login-form.ejs b/views/login-form.ejs index 34bf551..b0eef5e 100644 --- a/views/login-form.ejs +++ b/views/login-form.ejs @@ -5,7 +5,7 @@ Login Form - Lorex - + @@ -21,7 +21,7 @@ From 5eec473dec03853598e6ebc77a2c65e7c4439330 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Apr 2024 14:19:11 +0200 Subject: [PATCH 078/206] [Task] #58, dev tesing rule to disable --- src/client/.eslintrc.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/.eslintrc.json b/src/client/.eslintrc.json index 65c7867..fe23f14 100644 --- a/src/client/.eslintrc.json +++ b/src/client/.eslintrc.json @@ -26,5 +26,7 @@ "version": "detect" } }, - "rules": {} + "rules": { + //"react/jsx-key": false + } } \ No newline at end of file From 28af1bb1f995bdc7864cd98d5d786f4e7fff578f Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Apr 2024 14:19:50 +0200 Subject: [PATCH 079/206] [Task] #58, adjust styles for headline --- src/client/components/css/app.module.css | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/client/components/css/app.module.css b/src/client/components/css/app.module.css index a064782..6fb8c78 100644 --- a/src/client/components/css/app.module.css +++ b/src/client/components/css/app.module.css @@ -14,7 +14,7 @@ --text: var(--main); } - height: 100%; + min-height: 100%; color: var(--text); background: repeating-linear-gradient(45deg, var(--bg1), @@ -24,13 +24,12 @@ background-size: 200px 200px; animation: move-it 10s linear infinite; - display: flex; - justify-content: center; - align-items: center; - + .headline { + margin-inline: auto; + padding-block: max(1em, 10dvh); text-align: center; - font-size: clamp(30px, 5dvmax, 100px); + font-size: clamp(4rem, 5dvmax, 10rem); } } From a38e6fecc618f6bb056a4c02b3eaf810266756fa Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Apr 2024 14:20:30 +0200 Subject: [PATCH 080/206] [Task] #58, create Contacts wrapper Component --- src/client/components/Contacts.tsx | 40 +++++++++++++++++++ src/client/components/css/contacts.module.css | 6 +++ 2 files changed, 46 insertions(+) create mode 100644 src/client/components/Contacts.tsx create mode 100644 src/client/components/css/contacts.module.css diff --git a/src/client/components/Contacts.tsx b/src/client/components/Contacts.tsx new file mode 100644 index 0000000..49481ec --- /dev/null +++ b/src/client/components/Contacts.tsx @@ -0,0 +1,40 @@ +import React, { Component } from 'react'; +import * as css from "./css/contacts.module.css"; +import Contact from './Contact'; + +export default class Contacts extends Component<{}, client.Contacts> { + state: client.Contacts = { + contacts: [ + { + id: "0", + name: "John Doe", + email: "jd@example.com", + phone: "0123456789", + }, + { + id: "1", + name: "Joe Todd", + email: "jt@example.com", + phone: "0123456789", + hobby: "Swimming", + }, + { + id: "2", + name: "Julia Benner", + email: "jb@example.com", + phone: "0123456789", + hobby: "Running", + } + ] + } + + render() { + const { contacts } = this.state; + + return ( +
+ {contacts.map(contact => ())} +
+ ) + } +} diff --git a/src/client/components/css/contacts.module.css b/src/client/components/css/contacts.module.css new file mode 100644 index 0000000..00ab971 --- /dev/null +++ b/src/client/components/css/contacts.module.css @@ -0,0 +1,6 @@ +.contacts { + display: grid; + --rowGap: 2rem; + row-gap: var(--rowGap); +} + From 062f84cdbca605fc6a2e24f46e829fa1b85db50c Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Apr 2024 14:20:53 +0200 Subject: [PATCH 081/206] [Task] #58 apply wrapper component --- src/client/components/App.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 4b413e2..afa816a 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,13 +1,13 @@ import React, { Component } from 'react'; -import Contact from './Contact'; -import * as css from"./css/app.module.css"; +import Contacts from './Contacts'; +import * as css from "./css/app.module.css"; class App extends Component { render() { return (

Hello, React!

- +
); } From e68e30493d94634faf2acb64f613f0b31a9cb62b Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Apr 2024 14:21:16 +0200 Subject: [PATCH 082/206] [Task] #58 adjust contact component to expect object --- src/client/components/Contact.tsx | 23 +++++++++++--------- src/client/components/css/contact.module.css | 19 +++++++++++----- src/client/types.d.ts | 23 ++++++++++++++------ 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/client/components/Contact.tsx b/src/client/components/Contact.tsx index c7530f2..e8062cd 100644 --- a/src/client/components/Contact.tsx +++ b/src/client/components/Contact.tsx @@ -1,17 +1,22 @@ import React, { Component } from 'react'; -import * as classes from "./css/contact.module.css"; +import * as css from "./css/contact.module.css"; -export default class Contact extends Component { +export default class Contact extends Component { static defaultProps = { - name: "no name", - email: "no email", - hobby: "no hobby" + contact: { + name: "no name", + email: "no email", + hobby: "no hobby" + } } + render() { - const {name, email, phone, hobby} = this.props; - + let { name, email, phone, hobby } = this.props.contact; + hobby = hobby || "no hobby"; + + return ( -
+

{name}

Email:
{email}
@@ -22,5 +27,3 @@ export default class Contact extends Component { ) } } - -Contact \ No newline at end of file diff --git a/src/client/components/css/contact.module.css b/src/client/components/css/contact.module.css index 6e76527..6adf5a0 100644 --- a/src/client/components/css/contact.module.css +++ b/src/client/components/css/contact.module.css @@ -1,23 +1,30 @@ .contact { + font-size: 2rem; + + &:not(:first-child) { + padding-top: var(--rowGap, 2rem); + border-top: calc(var(--rowGap) / 10) solid; + } + h4 { text-align: center; + margin-bottom: 1rem; } - + dl { margin-inline: 0; list-style: none; display: grid; grid-template-columns: min-content min-content; justify-content: center; + gap: 1rem; } - + dt { grid-column: 1; - text-align: left; } + dd { grid-column: 2; - text-align: center; } -} - +} \ No newline at end of file diff --git a/src/client/types.d.ts b/src/client/types.d.ts index 6b2cf3f..251ebcd 100644 --- a/src/client/types.d.ts +++ b/src/client/types.d.ts @@ -2,10 +2,19 @@ declare module "*.module.css"; declare namespace client { - interface contact { - name: string, - email: string, - phone: string - hobby?: string - } -} \ No newline at end of file + interface Contact { + id: string; + name: string; + email: string; + phone: string; + hobby?: string; + } + + interface ContactProps { + contact: Contact; + } + + interface Contacts { + contacts: Contact[]; + } +} From 94e234c0839152201f47e93d7fcdfb1af87ad78c Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 22 Apr 2024 14:19:08 +0200 Subject: [PATCH 083/206] [Task] #58, toggle state --- src/client/components/Contact.tsx | 20 ++++++++++++++++---- src/client/components/css/contact.module.css | 3 ++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/client/components/Contact.tsx b/src/client/components/Contact.tsx index e8062cd..7144cec 100644 --- a/src/client/components/Contact.tsx +++ b/src/client/components/Contact.tsx @@ -2,27 +2,39 @@ import React, { Component } from 'react'; import * as css from "./css/contact.module.css"; export default class Contact extends Component { + state = { + expanded: false + } + + toggleDetails = () => { + this.setState({ + expanded: !this.state.expanded + }); + } + static defaultProps = { contact: { name: "no name", email: "no email", - hobby: "no hobby" + hobby: "no hobby" } } render() { let { name, email, phone, hobby } = this.props.contact; hobby = hobby || "no hobby"; + const { expanded } = this.state; return (
-

{name}

-
+

{name}

+ {expanded ? (
Email:
{email}
Phone:
{phone}
Hobby:
{hobby}
-
+
) : null} +
) } diff --git a/src/client/components/css/contact.module.css b/src/client/components/css/contact.module.css index 6adf5a0..ccf7b09 100644 --- a/src/client/components/css/contact.module.css +++ b/src/client/components/css/contact.module.css @@ -8,10 +8,11 @@ h4 { text-align: center; - margin-bottom: 1rem; + cursor: pointer; } dl { + margin-top: 1rem; margin-inline: 0; list-style: none; display: grid; From d7b57d8deede1523f7a1cf52481ace4dac31276b Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 23 Apr 2024 14:33:01 +0200 Subject: [PATCH 084/206] [Task] #58 learn context api provider and consumer --- src/client/components/App.tsx | 12 ++++-- src/client/components/Contact.tsx | 5 ++- src/client/components/Contacts.tsx | 44 +++++++------------- src/client/components/context.tsx | 42 +++++++++++++++++++ src/client/components/css/contact.module.css | 5 +++ src/client/types.d.ts | 7 +++- 6 files changed, 79 insertions(+), 36 deletions(-) create mode 100644 src/client/components/context.tsx diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index afa816a..824fd12 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -2,13 +2,17 @@ import React, { Component } from 'react'; import Contacts from './Contacts'; import * as css from "./css/app.module.css"; +import Provider from "./context" + class App extends Component { render() { return ( -
-

Hello, React!

- -
+ +
+

Hello, React!

+ +
+
); } } diff --git a/src/client/components/Contact.tsx b/src/client/components/Contact.tsx index 7144cec..f22d4e1 100644 --- a/src/client/components/Contact.tsx +++ b/src/client/components/Contact.tsx @@ -29,12 +29,13 @@ export default class Contact extends Component { return (

{name}

- {expanded ? (
+ {expanded ? (<>
Email:
{email}
Phone:
{phone}
Hobby:
{hobby}
-
) : null} +
+ ) : null}
) } diff --git a/src/client/components/Contacts.tsx b/src/client/components/Contacts.tsx index 49481ec..46bb9ba 100644 --- a/src/client/components/Contacts.tsx +++ b/src/client/components/Contacts.tsx @@ -1,40 +1,26 @@ import React, { Component } from 'react'; import * as css from "./css/contacts.module.css"; +import { Consumer } from './context'; import Contact from './Contact'; -export default class Contacts extends Component<{}, client.Contacts> { - state: client.Contacts = { - contacts: [ - { - id: "0", - name: "John Doe", - email: "jd@example.com", - phone: "0123456789", - }, - { - id: "1", - name: "Joe Todd", - email: "jt@example.com", - phone: "0123456789", - hobby: "Swimming", - }, - { - id: "2", - name: "Julia Benner", - email: "jb@example.com", - phone: "0123456789", - hobby: "Running", - } - ] - } +export default class Contacts extends Component { + render() { - const { contacts } = this.state; return ( -
- {contacts.map(contact => ())} -
+ + {value => { + const { contacts } = value; + return ( + <> +
+ {contacts.map(contact => ())} +
+ + ) + }} +
) } } diff --git a/src/client/components/context.tsx b/src/client/components/context.tsx new file mode 100644 index 0000000..92b0180 --- /dev/null +++ b/src/client/components/context.tsx @@ -0,0 +1,42 @@ +import React, { Component } from 'react'; + + +const Context = React.createContext(null); + +class Provider extends Component { + state: client.Contacts = { + contacts: [ + { + id: "0", + name: "John Doe", + email: "jd@example.com", + phone: "0123456789", + }, + { + id: "1", + name: "Joe Todd", + email: "jt@example.com", + phone: "0123456789", + hobby: "Swimming", + }, + { + id: "2", + name: "Julia Benner", + email: "jb@example.com", + phone: "0123456789", + hobby: "Running", + } + ] + } + + render() { + return ( + + {this.props.children} + + ) + } +} + +export const Consumer = Context.Consumer; +export default Provider; \ No newline at end of file diff --git a/src/client/components/css/contact.module.css b/src/client/components/css/contact.module.css index ccf7b09..ba2d114 100644 --- a/src/client/components/css/contact.module.css +++ b/src/client/components/css/contact.module.css @@ -28,4 +28,9 @@ dd { grid-column: 2; } + + button { + display: block; + margin: 1rem auto 0; + } } \ No newline at end of file diff --git a/src/client/types.d.ts b/src/client/types.d.ts index 251ebcd..03f90c2 100644 --- a/src/client/types.d.ts +++ b/src/client/types.d.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ declare module "*.module.css"; - declare namespace client { interface Contact { id: string; @@ -17,4 +16,10 @@ declare namespace client { interface Contacts { contacts: Contact[]; } + + interface ProviderProps { + children?: React.ReactNode; + } } + + From ed2fd14b58cf1b4a22eb5d2e4742fd222238db0c Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 23 Apr 2024 15:05:12 +0200 Subject: [PATCH 085/206] [Task] #58 add delete via dispatch --- src/client/components/Contact.tsx | 36 +++++++++++++++++++++---------- src/client/components/context.tsx | 21 ++++++++++++++++-- src/client/types.d.ts | 1 + 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/client/components/Contact.tsx b/src/client/components/Contact.tsx index f22d4e1..3ea71f4 100644 --- a/src/client/components/Contact.tsx +++ b/src/client/components/Contact.tsx @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import * as css from "./css/contact.module.css"; +import { Consumer } from './context'; export default class Contact extends Component { state = { @@ -12,6 +13,10 @@ export default class Contact extends Component { }); } + deleteContact = (id, dispatch) => { + dispatch({type: "DELETE_CONTACT", payload: id}) + } + static defaultProps = { contact: { name: "no name", @@ -21,22 +26,31 @@ export default class Contact extends Component { } render() { - let { name, email, phone, hobby } = this.props.contact; + let { id, name, email, phone, hobby } = this.props.contact; hobby = hobby || "no hobby"; const { expanded } = this.state; return ( -
-

{name}

- {expanded ? (<>
-
Email:
{email}
-
Phone:
{phone}
-
Hobby:
{hobby}
-
- - ) : null} -
+ + {value => { + const { dispatch } = value; + return ( +
+

{name}

+ {expanded ? (<>
+
Email:
{email}
+
Phone:
{phone}
+
Hobby:
{hobby}
+
+ + ) : null} +
+ ) + }} + +
+ ) } } diff --git a/src/client/components/context.tsx b/src/client/components/context.tsx index 92b0180..e96d196 100644 --- a/src/client/components/context.tsx +++ b/src/client/components/context.tsx @@ -1,8 +1,22 @@ import React, { Component } from 'react'; - const Context = React.createContext(null); +const reducer = (state, action) => { + switch (action.type) { + case 'DELETE_CONTACT': + return { + ...state, + contacts: state.contacts.filter( + contact => contact.id !== action.payload + ) + } + default: + return state + } +} + + class Provider extends Component { state: client.Contacts = { contacts: [ @@ -26,7 +40,10 @@ class Provider extends Component { phone: "0123456789", hobby: "Running", } - ] + ], + dispatch: (action) => { + this.setState(state => reducer(state,action)) + } } render() { diff --git a/src/client/types.d.ts b/src/client/types.d.ts index 03f90c2..e5ad066 100644 --- a/src/client/types.d.ts +++ b/src/client/types.d.ts @@ -15,6 +15,7 @@ declare namespace client { interface Contacts { contacts: Contact[]; + dispatch?: (state: Contacts, action: string) => void; } interface ProviderProps { From 9b5300118cd1f52eb97fdd1adc2a442047c7cbe0 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 30 Apr 2024 10:29:54 +0200 Subject: [PATCH 086/206] [Task] #58, react-router, move contacts to new url --- package-lock.json | 39 +++++++++++++++++++ package.json | 1 + src/app.ts | 4 +- src/client/components/App.tsx | 35 ++++++++++------- src/client/components/css/app.module.css | 9 +++++ src/client/components/css/contacts.module.css | 1 + 6 files changed, 73 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index ddd6690..78e2ade 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "module-alias": "^2.2.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.23.0", "toobusy-js": "^0.5.1" }, "devDependencies": { @@ -1530,6 +1531,14 @@ "node": ">= 8" } }, + "node_modules/@remix-run/router": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.0.tgz", + "integrity": "sha512-Quz1KOffeEf/zwkCBM3kBtH4ZoZ+pT3xIXBG4PPW/XFtDP7EGhtTiC2+gpL9GnR7+Qdet5Oa6cYSvwKYg6kN9Q==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -10958,6 +10967,36 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/react-router": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.0.tgz", + "integrity": "sha512-wPMZ8S2TuPadH0sF5irFGjkNLIcRvOSaEe7v+JER8508dyJumm6XZB1u5kztlX0RVq6AzRVndzqcUh6sFIauzA==", + "dependencies": { + "@remix-run/router": "1.16.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.0.tgz", + "integrity": "sha512-Q9YaSYvubwgbal2c9DJKfx6hTNoBp3iJDsl+Duva/DwxoJH+OTXkxGpql4iUK2sla/8z4RpjAm6EWx1qUDuopQ==", + "dependencies": { + "@remix-run/router": "1.16.0", + "react-router": "6.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", diff --git a/package.json b/package.json index 9e5607c..2d33c68 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "module-alias": "^2.2.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.23.0", "toobusy-js": "^0.5.1" }, "_moduleAliases": { diff --git a/src/app.ts b/src/app.ts index 798e48a..2f430e5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -54,9 +54,9 @@ app.use((req, res, next) => { // limit body for specific http methods // routes -app.get('/', (req, res) => { +app.get(['/', '/contacts'], (req, res) => { logger.log(req.ip + " - " + res.locals.ip, true); - res.render("index", {root: process.env.ROOT}); + res.render("index"); }); app.use('/write', writeRouter); diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 824fd12..76388f7 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,21 +1,28 @@ -import React, { Component } from 'react'; +import React from 'react'; +import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom'; import Contacts from './Contacts'; import * as css from "./css/app.module.css"; import Provider from "./context" -class App extends Component { - render() { - return ( - -
-

Hello, React!

- -
-
- ); - } +const App = () => { + return ( + +
+ + + } /> + +

Hello, React!
+ Go to Contacts

+ + } /> +
+
+
+
+ ); } - -export default App; \ No newline at end of file +export default App; diff --git a/src/client/components/css/app.module.css b/src/client/components/css/app.module.css index 6fb8c78..1971d58 100644 --- a/src/client/components/css/app.module.css +++ b/src/client/components/css/app.module.css @@ -15,6 +15,10 @@ } min-height: 100%; + + display: flex; + align-items: center; + color: var(--text); background: repeating-linear-gradient(45deg, var(--bg1), @@ -32,4 +36,9 @@ text-align: center; font-size: clamp(4rem, 5dvmax, 10rem); } + + a { + display: block; + font-size: 2rem; + } } diff --git a/src/client/components/css/contacts.module.css b/src/client/components/css/contacts.module.css index 00ab971..c992328 100644 --- a/src/client/components/css/contacts.module.css +++ b/src/client/components/css/contacts.module.css @@ -1,5 +1,6 @@ .contacts { display: grid; + width: 100%; --rowGap: 2rem; row-gap: var(--rowGap); } From fe49d5607a7f11583600fff869193607ee2ab951 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 3 May 2024 10:47:38 +0200 Subject: [PATCH 087/206] [Task] #58 fetch more contacts --- src/client/components/Contacts.tsx | 1 - src/client/components/context.tsx | 25 ++++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/client/components/Contacts.tsx b/src/client/components/Contacts.tsx index 46bb9ba..8957e4d 100644 --- a/src/client/components/Contacts.tsx +++ b/src/client/components/Contacts.tsx @@ -5,7 +5,6 @@ import Contact from './Contact'; export default class Contacts extends Component { - render() { return ( diff --git a/src/client/components/context.tsx b/src/client/components/context.tsx index e96d196..d6c6d98 100644 --- a/src/client/components/context.tsx +++ b/src/client/components/context.tsx @@ -42,10 +42,33 @@ class Provider extends Component { } ], dispatch: (action) => { - this.setState(state => reducer(state,action)) + this.setState(state => reducer(state, action)) } } + async componentDidMount() { + let response; + + try { + response = await fetch('https://jsonplaceholder.typicode.com/users'); + } catch (error) { + console.log('There was an error', error); + } + + // Uses the 'optional chaining' operator + if (response?.ok) { + console.log('Use the response here!'); + const fetchedContacts = await response.json(); + const newContacts = this.state.contacts.concat(fetchedContacts); + + this.setState({...this.state, contacts: newContacts}) + + } else { + console.log(`HTTP Response Code: ${response?.status}`) + } + + } + render() { return ( From 272bbcefc61fe7026276f3e6f7719f03422fd705 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 3 May 2024 14:34:54 +0200 Subject: [PATCH 088/206] fix: package.json & package-lock.json to reduce vulnerabilities (#62) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-EJS-6689533 Co-authored-by: snyk-bot --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index ddd6690..6473913 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "chalk": "^4.1.2", "compression": "^1.7.4", "dotenv": "^16.4.5", - "ejs": "^3.1.9", + "ejs": "^3.1.10", "express": "^4.19.2", "express-rate-limit": "^7.2.0", "express-slow-down": "^2.0.1", @@ -3849,9 +3849,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dependencies": { "jake": "^10.8.5" }, diff --git a/package.json b/package.json index 9e5607c..8d674be 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "chalk": "^4.1.2", "compression": "^1.7.4", "dotenv": "^16.4.5", - "ejs": "^3.1.9", + "ejs": "^3.1.10", "express": "^4.19.2", "express-rate-limit": "^7.2.0", "express-slow-down": "^2.0.1", From 01c30fd5dceb86810f9e87443545886fc3ea2f90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 14:35:15 +0200 Subject: [PATCH 089/206] Bump tar and npm (#60) Bumps [tar](https://github.com/isaacs/node-tar) to 6.2.1 and updates ancestor dependency [npm](https://github.com/npm/cli). These dependencies need to be updated together. Updates `tar` from 6.2.0 to 6.2.1 - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1) Updates `npm` from 10.5.0 to 10.5.2 - [Release notes](https://github.com/npm/cli/releases) - [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md) - [Commits](https://github.com/npm/cli/compare/v10.5.0...v10.5.2) --- updated-dependencies: - dependency-name: tar dependency-type: indirect - dependency-name: npm dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 238 ++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 138 insertions(+), 102 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6473913..1fb0d1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "install": "^0.13.0", "jest": "^29.7.0", "nodemon": "^3.0.2", - "npm": "^10.5.0", + "npm": "^10.5.2", "style-loader": "^4.0.0", "ts-jest": "^29.1.2", "ts-loader": "^9.5.1", @@ -7452,9 +7452,9 @@ } }, "node_modules/npm": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-10.5.0.tgz", - "integrity": "sha512-Ejxwvfh9YnWVU2yA5FzoYLTW52vxHCz+MHrOFg9Cc8IFgF/6f5AGPAvb5WTay5DIUP1NIfN3VBZ0cLlGO0Ys+A==", + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.5.2.tgz", + "integrity": "sha512-cHVG7QEJwJdZyOrK0dKX5uf3R5Fd0E8AcmSES1jLtO52UT1enUKZ96Onw/xwq4CbrTZEnDuu2Vf9kCQh/Sd12w==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -7463,6 +7463,7 @@ "@npmcli/map-workspaces", "@npmcli/package-json", "@npmcli/promise-spawn", + "@npmcli/redact", "@npmcli/run-script", "@sigstore/tuf", "abbrev", @@ -7533,27 +7534,28 @@ "@npmcli/arborist": "^7.2.1", "@npmcli/config": "^8.0.2", "@npmcli/fs": "^3.1.0", - "@npmcli/map-workspaces": "^3.0.4", - "@npmcli/package-json": "^5.0.0", + "@npmcli/map-workspaces": "^3.0.6", + "@npmcli/package-json": "^5.0.2", "@npmcli/promise-spawn": "^7.0.1", + "@npmcli/redact": "^1.1.0", "@npmcli/run-script": "^7.0.4", - "@sigstore/tuf": "^2.3.1", + "@sigstore/tuf": "^2.3.2", "abbrev": "^2.0.0", "archy": "~1.0.0", "cacache": "^18.0.2", "chalk": "^5.3.0", "ci-info": "^4.0.0", "cli-columns": "^4.0.0", - "cli-table3": "^0.6.3", + "cli-table3": "^0.6.4", "columnify": "^1.6.0", "fastest-levenshtein": "^1.0.16", "fs-minipass": "^3.0.3", - "glob": "^10.3.10", + "glob": "^10.3.12", "graceful-fs": "^4.2.11", "hosted-git-info": "^7.0.1", - "ini": "^4.1.1", - "init-package-json": "^6.0.0", - "is-cidr": "^5.0.3", + "ini": "^4.1.2", + "init-package-json": "^6.0.2", + "is-cidr": "^5.0.5", "json-parse-even-better-errors": "^3.0.1", "libnpmaccess": "^8.0.1", "libnpmdiff": "^6.0.3", @@ -7567,11 +7569,11 @@ "libnpmteam": "^6.0.0", "libnpmversion": "^5.0.1", "make-fetch-happen": "^13.0.0", - "minimatch": "^9.0.3", + "minimatch": "^9.0.4", "minipass": "^7.0.4", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^10.0.1", + "node-gyp": "^10.1.0", "nopt": "^7.2.0", "normalize-package-data": "^6.0.0", "npm-audit-report": "^5.0.0", @@ -7579,7 +7581,7 @@ "npm-package-arg": "^11.0.1", "npm-pick-manifest": "^9.0.0", "npm-profile": "^9.0.0", - "npm-registry-fetch": "^16.1.0", + "npm-registry-fetch": "^16.2.0", "npm-user-validate": "^2.0.0", "npmlog": "^7.0.1", "p-map": "^4.0.0", @@ -7587,12 +7589,12 @@ "parse-conflict-json": "^3.0.1", "proc-log": "^3.0.0", "qrcode-terminal": "^0.12.0", - "read": "^2.1.0", + "read": "^3.0.1", "semver": "^7.6.0", - "spdx-expression-parse": "^3.0.1", + "spdx-expression-parse": "^4.0.0", "ssri": "^10.0.5", "supports-color": "^9.4.0", - "tar": "^6.2.0", + "tar": "^6.2.1", "text-table": "~0.2.0", "tiny-relative-date": "^1.3.0", "treeverse": "^3.0.0", @@ -7704,7 +7706,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/@npmcli/agent": { - "version": "2.2.1", + "version": "2.2.2", "dev": true, "inBundle": true, "license": "ISC", @@ -7713,14 +7715,14 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.3" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "7.4.0", + "version": "7.4.2", "dev": true, "inBundle": true, "license": "ISC", @@ -7734,6 +7736,7 @@ "@npmcli/node-gyp": "^3.0.0", "@npmcli/package-json": "^5.0.0", "@npmcli/query": "^3.1.0", + "@npmcli/redact": "^1.1.0", "@npmcli/run-script": "^7.0.2", "bin-links": "^4.0.1", "cacache": "^18.0.0", @@ -7741,12 +7744,12 @@ "hosted-git-info": "^7.0.1", "json-parse-even-better-errors": "^3.0.0", "json-stringify-nice": "^1.1.4", - "minimatch": "^9.0.0", + "minimatch": "^9.0.4", "nopt": "^7.0.0", "npm-install-checks": "^6.2.0", "npm-package-arg": "^11.0.1", "npm-pick-manifest": "^9.0.0", - "npm-registry-fetch": "^16.0.0", + "npm-registry-fetch": "^16.2.0", "npmlog": "^7.0.1", "pacote": "^17.0.4", "parse-conflict-json": "^3.0.0", @@ -7767,14 +7770,14 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "8.2.0", + "version": "8.2.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/map-workspaces": "^3.0.2", "ci-info": "^4.0.0", - "ini": "^4.1.0", + "ini": "^4.1.2", "nopt": "^7.0.0", "proc-log": "^3.0.0", "read-package-json-fast": "^3.0.2", @@ -7825,7 +7828,7 @@ } }, "node_modules/npm/node_modules/@npmcli/git": { - "version": "5.0.4", + "version": "5.0.5", "dev": true, "inBundle": true, "license": "ISC", @@ -7860,7 +7863,7 @@ } }, "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "3.0.4", + "version": "3.0.6", "dev": true, "inBundle": true, "license": "ISC", @@ -7908,7 +7911,7 @@ } }, "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "5.0.0", + "version": "5.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -7949,6 +7952,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/npm/node_modules/@npmcli/run-script": { "version": "7.0.4", "dev": true, @@ -7976,19 +7988,19 @@ } }, "node_modules/npm/node_modules/@sigstore/bundle": { - "version": "2.2.0", + "version": "2.3.1", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.3.0" + "@sigstore/protobuf-specs": "^0.3.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@sigstore/core": { - "version": "1.0.0", + "version": "1.1.0", "dev": true, "inBundle": true, "license": "Apache-2.0", @@ -7997,23 +8009,23 @@ } }, "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.3.0", + "version": "0.3.1", "dev": true, "inBundle": true, "license": "Apache-2.0", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@sigstore/sign": { - "version": "2.2.3", + "version": "2.3.0", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.2.0", + "@sigstore/bundle": "^2.3.0", "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.0", + "@sigstore/protobuf-specs": "^0.3.1", "make-fetch-happen": "^13.0.0" }, "engines": { @@ -8021,7 +8033,7 @@ } }, "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "2.3.1", + "version": "2.3.2", "dev": true, "inBundle": true, "license": "Apache-2.0", @@ -8034,14 +8046,14 @@ } }, "node_modules/npm/node_modules/@sigstore/verify": { - "version": "1.1.0", + "version": "1.2.0", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.2.0", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.0" + "@sigstore/bundle": "^2.3.1", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -8079,7 +8091,7 @@ } }, "node_modules/npm/node_modules/agent-base": { - "version": "7.1.0", + "version": "7.1.1", "dev": true, "inBundle": true, "license": "MIT", @@ -8167,12 +8179,15 @@ } }, "node_modules/npm/node_modules/binary-extensions": { - "version": "2.2.0", + "version": "2.3.0", "dev": true, "inBundle": true, "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/npm/node_modules/brace-expansion": { @@ -8185,7 +8200,7 @@ } }, "node_modules/npm/node_modules/builtins": { - "version": "5.0.1", + "version": "5.1.0", "dev": true, "inBundle": true, "license": "MIT", @@ -8253,7 +8268,7 @@ } }, "node_modules/npm/node_modules/cidr-regex": { - "version": "4.0.3", + "version": "4.0.5", "dev": true, "inBundle": true, "license": "BSD-2-Clause", @@ -8287,7 +8302,7 @@ } }, "node_modules/npm/node_modules/cli-table3": { - "version": "0.6.3", + "version": "0.6.4", "dev": true, "inBundle": true, "license": "MIT", @@ -8565,16 +8580,16 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "10.3.10", + "version": "10.3.12", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -8599,7 +8614,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/hasown": { - "version": "2.0.1", + "version": "2.0.2", "dev": true, "inBundle": true, "license": "MIT", @@ -8698,7 +8713,7 @@ } }, "node_modules/npm/node_modules/ini": { - "version": "4.1.1", + "version": "4.1.2", "dev": true, "inBundle": true, "license": "ISC", @@ -8707,15 +8722,15 @@ } }, "node_modules/npm/node_modules/init-package-json": { - "version": "6.0.0", + "version": "6.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { + "@npmcli/package-json": "^5.0.0", "npm-package-arg": "^11.0.0", "promzard": "^1.0.0", - "read": "^2.0.0", - "read-package-json": "^7.0.0", + "read": "^3.0.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4", "validate-npm-package-name": "^5.0.0" @@ -8756,12 +8771,12 @@ } }, "node_modules/npm/node_modules/is-cidr": { - "version": "5.0.3", + "version": "5.0.5", "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { - "cidr-regex": "4.0.3" + "cidr-regex": "^4.0.4" }, "engines": { "node": ">=14" @@ -8864,20 +8879,20 @@ "license": "MIT" }, "node_modules/npm/node_modules/libnpmaccess": { - "version": "8.0.2", + "version": "8.0.3", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "npm-package-arg": "^11.0.1", - "npm-registry-fetch": "^16.0.0" + "npm-registry-fetch": "^16.2.0" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "6.0.7", + "version": "6.0.9", "dev": true, "inBundle": true, "license": "ISC", @@ -8885,19 +8900,19 @@ "@npmcli/arborist": "^7.2.1", "@npmcli/disparity-colors": "^3.0.0", "@npmcli/installed-package-contents": "^2.0.2", - "binary-extensions": "^2.2.0", + "binary-extensions": "^2.3.0", "diff": "^5.1.0", - "minimatch": "^9.0.0", + "minimatch": "^9.0.4", "npm-package-arg": "^11.0.1", "pacote": "^17.0.4", - "tar": "^6.2.0" + "tar": "^6.2.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "7.0.8", + "version": "7.0.10", "dev": true, "inBundle": true, "license": "ISC", @@ -8909,7 +8924,7 @@ "npmlog": "^7.0.1", "pacote": "^17.0.4", "proc-log": "^3.0.0", - "read": "^2.0.0", + "read": "^3.0.1", "read-package-json-fast": "^3.0.2", "semver": "^7.3.7", "walk-up-path": "^3.0.1" @@ -8919,7 +8934,7 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "5.0.5", + "version": "5.0.7", "dev": true, "inBundle": true, "license": "ISC", @@ -8931,33 +8946,33 @@ } }, "node_modules/npm/node_modules/libnpmhook": { - "version": "10.0.1", + "version": "10.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "aproba": "^2.0.0", - "npm-registry-fetch": "^16.0.0" + "npm-registry-fetch": "^16.2.0" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmorg": { - "version": "6.0.2", + "version": "6.0.3", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "aproba": "^2.0.0", - "npm-registry-fetch": "^16.0.0" + "npm-registry-fetch": "^16.2.0" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "6.0.7", + "version": "6.0.9", "dev": true, "inBundle": true, "license": "ISC", @@ -8972,7 +8987,7 @@ } }, "node_modules/npm/node_modules/libnpmpublish": { - "version": "9.0.4", + "version": "9.0.5", "dev": true, "inBundle": true, "license": "ISC", @@ -8980,7 +8995,7 @@ "ci-info": "^4.0.0", "normalize-package-data": "^6.0.0", "npm-package-arg": "^11.0.1", - "npm-registry-fetch": "^16.0.0", + "npm-registry-fetch": "^16.2.0", "proc-log": "^3.0.0", "semver": "^7.3.7", "sigstore": "^2.2.0", @@ -8991,25 +9006,25 @@ } }, "node_modules/npm/node_modules/libnpmsearch": { - "version": "7.0.1", + "version": "7.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "npm-registry-fetch": "^16.0.0" + "npm-registry-fetch": "^16.2.0" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmteam": { - "version": "6.0.1", + "version": "6.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "aproba": "^2.0.0", - "npm-registry-fetch": "^16.0.0" + "npm-registry-fetch": "^16.2.0" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -9063,7 +9078,7 @@ } }, "node_modules/npm/node_modules/minimatch": { - "version": "9.0.3", + "version": "9.0.4", "dev": true, "inBundle": true, "license": "ISC", @@ -9271,7 +9286,7 @@ } }, "node_modules/npm/node_modules/node-gyp": { - "version": "10.0.1", + "version": "10.1.0", "dev": true, "inBundle": true, "license": "MIT", @@ -9422,11 +9437,12 @@ } }, "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "16.1.0", + "version": "16.2.0", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { + "@npmcli/redact": "^1.1.0", "make-fetch-happen": "^13.0.0", "minipass": "^7.0.2", "minipass-fetch": "^3.0.0", @@ -9534,12 +9550,12 @@ } }, "node_modules/npm/node_modules/path-scurry": { - "version": "1.10.1", + "version": "1.10.2", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { @@ -9550,7 +9566,7 @@ } }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "6.0.16", "dev": true, "inBundle": true, "license": "MIT", @@ -9609,12 +9625,12 @@ } }, "node_modules/npm/node_modules/promzard": { - "version": "1.0.0", + "version": "1.0.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "read": "^2.0.0" + "read": "^3.0.1" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -9629,12 +9645,12 @@ } }, "node_modules/npm/node_modules/read": { - "version": "2.1.0", + "version": "3.0.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "mute-stream": "~1.0.0" + "mute-stream": "^1.0.0" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -9760,17 +9776,17 @@ } }, "node_modules/npm/node_modules/sigstore": { - "version": "2.2.2", + "version": "2.3.0", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.2.0", + "@sigstore/bundle": "^2.3.1", "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.0", - "@sigstore/sign": "^2.2.3", + "@sigstore/protobuf-specs": "^0.3.1", + "@sigstore/sign": "^2.3.0", "@sigstore/tuf": "^2.3.1", - "@sigstore/verify": "^1.1.0" + "@sigstore/verify": "^1.2.0" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -9787,7 +9803,7 @@ } }, "node_modules/npm/node_modules/socks": { - "version": "2.8.0", + "version": "2.8.3", "dev": true, "inBundle": true, "license": "MIT", @@ -9796,17 +9812,17 @@ "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 16.0.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/npm/node_modules/socks-proxy-agent": { - "version": "8.0.2", + "version": "8.0.3", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.1", "debug": "^4.3.4", "socks": "^2.7.1" }, @@ -9824,6 +9840,16 @@ "spdx-license-ids": "^3.0.0" } }, + "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/npm/node_modules/spdx-exceptions": { "version": "2.5.0", "dev": true, @@ -9831,7 +9857,7 @@ "license": "CC-BY-3.0" }, "node_modules/npm/node_modules/spdx-expression-parse": { - "version": "3.0.1", + "version": "4.0.0", "dev": true, "inBundle": true, "license": "MIT", @@ -9925,7 +9951,7 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "6.2.0", + "version": "6.2.1", "dev": true, "inBundle": true, "license": "ISC", @@ -10049,6 +10075,16 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/npm/node_modules/validate-npm-package-name": { "version": "5.0.0", "dev": true, @@ -11718,9 +11754,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", diff --git a/package.json b/package.json index 8d674be..e361b4e 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "install": "^0.13.0", "jest": "^29.7.0", "nodemon": "^3.0.2", - "npm": "^10.5.0", + "npm": "^10.5.2", "style-loader": "^4.0.0", "ts-jest": "^29.1.2", "ts-loader": "^9.5.1", From 5eb9c722670135cfe107ddc0b1212d591799eed0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 14:35:30 +0200 Subject: [PATCH 090/206] Bump ejs from 3.1.9 to 3.1.10 (#63) Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10. - [Release notes](https://github.com/mde/ejs/releases) - [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10) --- updated-dependencies: - dependency-name: ejs dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 0d2bbbe51098c9f94a4888688b2a27071dd87dc6 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 6 May 2024 14:31:23 +0200 Subject: [PATCH 091/206] [Task] #58, webpack configuriation to allow regular css files as well as modules --- src/client/components/App.tsx | 6 +++--- .../components/css/{app.module.css => app.css} | 0 webpack.config.js | 13 ++++++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) rename src/client/components/css/{app.module.css => app.css} (100%) diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 76388f7..d6142f3 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,20 +1,20 @@ import React from 'react'; import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom'; import Contacts from './Contacts'; -import * as css from "./css/app.module.css"; +import "./css/app.css"; import Provider from "./context" const App = () => { return ( -
+
} /> -

Hello, React!
+

Hello, React!
Go to Contacts

} /> diff --git a/src/client/components/css/app.module.css b/src/client/components/css/app.css similarity index 100% rename from src/client/components/css/app.module.css rename to src/client/components/css/app.css diff --git a/webpack.config.js b/webpack.config.js index 18b4969..4e5df71 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -20,15 +20,22 @@ module.exports = (args) => { { test: /\.css$/, use: [ - 'style-loader', + "style-loader", { - loader: 'css-loader', + loader: "css-loader", options: { + importLoaders: 1, modules: true, }, }, ], - } + include: /\.module\.css$/, + }, + { + test: /\.css$/, + use: ["style-loader", "css-loader"], + exclude: /\.module\.css$/, + }, ], }, resolve: { From 49c6d81f8ab6b43042e113f11419b5c8183c2e6a Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 7 May 2024 10:20:33 +0200 Subject: [PATCH 092/206] [Task] #58, clean up react learn files --- src/app.ts | 2 +- src/client/components/App.tsx | 22 +---- src/client/components/Contact.tsx | 56 ------------- src/client/components/Contacts.tsx | 25 ------ src/client/components/context.tsx | 82 ------------------- src/client/components/css/contact.module.css | 36 -------- src/client/components/css/contacts.module.css | 7 -- src/client/types.d.ts | 19 ----- 8 files changed, 5 insertions(+), 244 deletions(-) delete mode 100644 src/client/components/Contact.tsx delete mode 100644 src/client/components/Contacts.tsx delete mode 100644 src/client/components/context.tsx delete mode 100644 src/client/components/css/contact.module.css delete mode 100644 src/client/components/css/contacts.module.css diff --git a/src/app.ts b/src/app.ts index 2f430e5..0636521 100644 --- a/src/app.ts +++ b/src/app.ts @@ -54,7 +54,7 @@ app.use((req, res, next) => { // limit body for specific http methods // routes -app.get(['/', '/contacts'], (req, res) => { +app.get(['/'], (req, res) => { logger.log(req.ip + " - " + res.locals.ip, true); res.render("index"); }); diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index d6142f3..8c25b18 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,27 +1,13 @@ import React from 'react'; -import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom'; -import Contacts from './Contacts'; +//import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom'; import "./css/app.css"; -import Provider from "./context" const App = () => { return ( - -
- - - } /> - -

Hello, React!
- Go to Contacts

- - } /> -
-
-
-
+
+

Hello, React!

+
); } diff --git a/src/client/components/Contact.tsx b/src/client/components/Contact.tsx deleted file mode 100644 index 3ea71f4..0000000 --- a/src/client/components/Contact.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { Component } from 'react'; -import * as css from "./css/contact.module.css"; -import { Consumer } from './context'; - -export default class Contact extends Component { - state = { - expanded: false - } - - toggleDetails = () => { - this.setState({ - expanded: !this.state.expanded - }); - } - - deleteContact = (id, dispatch) => { - dispatch({type: "DELETE_CONTACT", payload: id}) - } - - static defaultProps = { - contact: { - name: "no name", - email: "no email", - hobby: "no hobby" - } - } - - render() { - let { id, name, email, phone, hobby } = this.props.contact; - hobby = hobby || "no hobby"; - const { expanded } = this.state; - - - return ( - - {value => { - const { dispatch } = value; - return ( -
-

{name}

- {expanded ? (<>
-
Email:
{email}
-
Phone:
{phone}
-
Hobby:
{hobby}
-
- - ) : null} -
- ) - }} - -
- - ) - } -} diff --git a/src/client/components/Contacts.tsx b/src/client/components/Contacts.tsx deleted file mode 100644 index 8957e4d..0000000 --- a/src/client/components/Contacts.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { Component } from 'react'; -import * as css from "./css/contacts.module.css"; -import { Consumer } from './context'; -import Contact from './Contact'; - -export default class Contacts extends Component { - - render() { - - return ( - - {value => { - const { contacts } = value; - return ( - <> -
- {contacts.map(contact => ())} -
- - ) - }} -
- ) - } -} diff --git a/src/client/components/context.tsx b/src/client/components/context.tsx deleted file mode 100644 index d6c6d98..0000000 --- a/src/client/components/context.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { Component } from 'react'; - -const Context = React.createContext(null); - -const reducer = (state, action) => { - switch (action.type) { - case 'DELETE_CONTACT': - return { - ...state, - contacts: state.contacts.filter( - contact => contact.id !== action.payload - ) - } - default: - return state - } -} - - -class Provider extends Component { - state: client.Contacts = { - contacts: [ - { - id: "0", - name: "John Doe", - email: "jd@example.com", - phone: "0123456789", - }, - { - id: "1", - name: "Joe Todd", - email: "jt@example.com", - phone: "0123456789", - hobby: "Swimming", - }, - { - id: "2", - name: "Julia Benner", - email: "jb@example.com", - phone: "0123456789", - hobby: "Running", - } - ], - dispatch: (action) => { - this.setState(state => reducer(state, action)) - } - } - - async componentDidMount() { - let response; - - try { - response = await fetch('https://jsonplaceholder.typicode.com/users'); - } catch (error) { - console.log('There was an error', error); - } - - // Uses the 'optional chaining' operator - if (response?.ok) { - console.log('Use the response here!'); - const fetchedContacts = await response.json(); - const newContacts = this.state.contacts.concat(fetchedContacts); - - this.setState({...this.state, contacts: newContacts}) - - } else { - console.log(`HTTP Response Code: ${response?.status}`) - } - - } - - render() { - return ( - - {this.props.children} - - ) - } -} - -export const Consumer = Context.Consumer; -export default Provider; \ No newline at end of file diff --git a/src/client/components/css/contact.module.css b/src/client/components/css/contact.module.css deleted file mode 100644 index ba2d114..0000000 --- a/src/client/components/css/contact.module.css +++ /dev/null @@ -1,36 +0,0 @@ -.contact { - font-size: 2rem; - - &:not(:first-child) { - padding-top: var(--rowGap, 2rem); - border-top: calc(var(--rowGap) / 10) solid; - } - - h4 { - text-align: center; - cursor: pointer; - } - - dl { - margin-top: 1rem; - margin-inline: 0; - list-style: none; - display: grid; - grid-template-columns: min-content min-content; - justify-content: center; - gap: 1rem; - } - - dt { - grid-column: 1; - } - - dd { - grid-column: 2; - } - - button { - display: block; - margin: 1rem auto 0; - } -} \ No newline at end of file diff --git a/src/client/components/css/contacts.module.css b/src/client/components/css/contacts.module.css deleted file mode 100644 index c992328..0000000 --- a/src/client/components/css/contacts.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.contacts { - display: grid; - width: 100%; - --rowGap: 2rem; - row-gap: var(--rowGap); -} - diff --git a/src/client/types.d.ts b/src/client/types.d.ts index e5ad066..6b48c70 100644 --- a/src/client/types.d.ts +++ b/src/client/types.d.ts @@ -1,26 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ declare module "*.module.css"; declare namespace client { - interface Contact { - id: string; - name: string; - email: string; - phone: string; - hobby?: string; - } - interface ContactProps { - contact: Contact; - } - - interface Contacts { - contacts: Contact[]; - dispatch?: (state: Contacts, action: string) => void; - } - - interface ProviderProps { - children?: React.ReactNode; - } } From 2bf0d91fc27d53c24942adb2d88ef1291961870a Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 7 May 2024 11:11:37 +0200 Subject: [PATCH 093/206] [Task] #58, setup react router --- src/client/components/App.tsx | 1 + src/client/index.tsx | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 8c25b18..a9c6b1a 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -12,3 +12,4 @@ const App = () => { } export default App; + diff --git a/src/client/index.tsx b/src/client/index.tsx index d542e5f..66253da 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,12 +1,24 @@ import * as React from 'react'; import { Root, createRoot } from 'react-dom/client'; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; import App from "./components/App"; +const router = createBrowserRouter([ + { + path: "/", + element: , + } +]); + const container = document.getElementById('react-root'); -let root:Root; +let root: Root; if (container) { root = createRoot(container); - root.render(); + root.render( + + ); } else { console.error("root not found"); } + + From 56835ea31bb680641265b403e4956ab173f5c8c5 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 10 May 2024 13:06:56 +0200 Subject: [PATCH 094/206] [Task] #61 install Material UI --- package-lock.json | 607 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 4 + 2 files changed, 558 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index a8cc737..4e7c66e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "lorex", "version": "0.0.1", "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@mui/icons-material": "^5.15.16", + "@mui/material": "^5.15.16", "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", @@ -89,7 +93,6 @@ "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, "dependencies": { "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" @@ -102,7 +105,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -114,7 +116,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -128,7 +129,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -136,14 +136,12 @@ "node_modules/@babel/code-frame/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -322,7 +320,6 @@ "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, "dependencies": { "@babel/types": "^7.22.15" }, @@ -386,7 +383,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -395,7 +391,6 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -427,7 +422,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -441,7 +435,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -453,7 +446,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -467,7 +459,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -475,14 +466,12 @@ "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -680,7 +669,6 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -759,7 +747,6 @@ "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", - "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -796,6 +783,152 @@ "node": ">=10.0.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/styled": { + "version": "11.11.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", + "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.2", + "@emotion/serialize": "^1.1.4", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -875,6 +1008,40 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.1.tgz", + "integrity": "sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.9.tgz", + "integrity": "sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -1496,6 +1663,251 @@ "node": ">=6" } }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.15.16", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.16.tgz", + "integrity": "sha512-PTIbMJs5C/vYMfyJNW8ArOezh4eyHkg2pTeA7bBxh2kLP1Uzs0Nm+krXWbWGJPwTWjM8EhnDrr4aCF26+2oleg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.15.16", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.16.tgz", + "integrity": "sha512-s8vYbyACzTNZRKv+20fCfVXJwJqNcVotns2EKnu1wmAga6wv2LAo5kB1d5yqQqZlMFtp34EJvRXf7cy8X0tJVA==", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.15.16", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.16.tgz", + "integrity": "sha512-ery2hFReewko9gpDBqOr2VmXwQG9ifXofPhGzIx09/b9JqCQC/06kZXZDGGrOTpIddK9HlIf4yrS+G70jPAzUQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/core-downloads-tracker": "^5.15.16", + "@mui/system": "^5.15.15", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", + "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.15.14", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", + "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz", + "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.15.14", + "@mui/styled-engine": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.14", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", + "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", + "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1531,6 +1943,15 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@remix-run/router": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.0.tgz", @@ -1815,11 +2236,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/qs": { "version": "6.9.11", @@ -1837,7 +2262,6 @@ "version": "18.2.74", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.74.tgz", "integrity": "sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==", - "dev": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1852,6 +2276,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", @@ -2919,6 +3351,20 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", @@ -3123,7 +3569,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -3293,6 +3738,14 @@ "node": ">=6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3497,6 +3950,21 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -3588,8 +4056,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -3833,6 +4300,15 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -3931,7 +4407,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -4116,7 +4591,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -5002,6 +5476,11 @@ "node": ">= 0.8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5398,7 +5877,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -5475,6 +5953,19 @@ "node": ">=16.0.0" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/hpp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", @@ -5592,7 +6083,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -5705,8 +6195,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-async-function": { "version": "2.0.0", @@ -5779,7 +6268,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -6863,8 +7351,7 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -7020,8 +7507,7 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/loader-runner": { "version": "4.3.0", @@ -10509,7 +10995,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -10521,7 +11006,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -10572,8 +11056,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-to-regexp": { "version": "0.1.7", @@ -10584,7 +11067,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -10847,7 +11329,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -10857,8 +11338,7 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -11000,8 +11480,7 @@ "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-router": { "version": "6.23.0", @@ -11033,6 +11512,21 @@ "react-dom": ">=16.8" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -11094,8 +11588,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", @@ -11128,7 +11621,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -11166,7 +11658,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -11759,11 +12250,15 @@ "webpack": "^5.27.0" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -11775,7 +12270,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -11948,7 +12442,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, "engines": { "node": ">=4" } @@ -12791,6 +13284,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 55348d6..a673eba 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,10 @@ "webpack-cli": "^5.1.4" }, "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@mui/icons-material": "^5.15.16", + "@mui/material": "^5.15.16", "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", From d47d0a277b43fabf15af71003522ca4916c56810 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 10 May 2024 13:07:52 +0200 Subject: [PATCH 095/206] [Task] #61, test mui --- src/client/components/App.tsx | 5 ++++- src/client/components/css/app.css | 14 ++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index a9c6b1a..3205e16 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,12 +1,15 @@ import React from 'react'; -//import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom'; import "./css/app.css"; +import { Button, Typography } from '@mui/material'; const App = () => { return (

Hello, React!

+ Test Headline + +
); } diff --git a/src/client/components/css/app.css b/src/client/components/css/app.css index 1971d58..ddf6ba5 100644 --- a/src/client/components/css/app.css +++ b/src/client/components/css/app.css @@ -5,19 +5,20 @@ } .app { - --bg1: color-mix(in oklch, var(--main-L6) 80%, var(--main-L5)); - --bg2: var(--main-L6); - --text: var(--neutral-L1); + --bg1: color-mix(in oklch, var(--main) 95%, white); + --bg2: var(--main); + --text: color-mix(in oklch, var(--neutral) 50%, black);; @media (prefers-color-scheme: dark) { - --bg1: var(--neutral-L1); - --bg2: var(--neutral-L2); + --bg1: color-mix(in oklch, var(--neutral) 90%, black); + --bg2: var(--neutral); --text: var(--main); } min-height: 100%; - display: flex; + justify-content: center; align-items: center; + flex-wrap: wrap; color: var(--text); background: repeating-linear-gradient(45deg, @@ -35,6 +36,7 @@ padding-block: max(1em, 10dvh); text-align: center; font-size: clamp(4rem, 5dvmax, 10rem); + flex-basis: 100%; } a { From f058aa522cdab131243799b4584ebfb814a9d873 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 10 May 2024 13:11:29 +0200 Subject: [PATCH 096/206] [CHANGE, MultiLine] #61 color variables levels removed, MUI Overwrites introduced color variables levels are replaced by color-mix. MUI Experimental API with Variables is used and to overwrite theme colors in css (since I want CSS to be single source of truth for colors) --- httpdocs/css/base.css | 233 +++++++++++++++++++++++++++++++++--------- 1 file changed, 186 insertions(+), 47 deletions(-) diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index b5c67c0..ab89268 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -7,7 +7,8 @@ Project Name: LOREX *2. Global styles / Variables *3. Helper styles *4. Grid styles -*5. Media Queries +*5. Overrides +*6. Media Queries ----------------------------------------------------------------------- */ /* ============================== @@ -113,50 +114,19 @@ Success: #59ec04 Neutral: #131211 */ :root { - --main: var(--main-L6); - --main-L1: oklch(10% 0.02 64.55); - --main-L2: oklch(25% 0.056 64.55); - --main-L3: oklch(37.5% 0.085 64.55); - --main-L4: oklch(50% 0.114 64.55); - --main-L5: oklch(62.5% 0.142 64.55); - --main-L6: oklch(77.2% 0.1738 64.55); /* base */ - --main-L7: oklch(90% 0.06 64.55); - - --info: var(--info-L4); - --info-L1: oklch(10% 0.055 268.01); - --info-L2: oklch(25% 0.158 268.01); - --info-L3: oklch(37.5% 0.237 268.01); - --info-L4: oklch(50% 0.2838 268.01); /* base */ - --info-L5: oklch(62.5% 0.19 268.01); - --info-L6: oklch(77.2% 0.109 268.01); - --info-L7: oklch(90% 0.04 268.01); - - --alert: var(--alert-L5); - --alert-L1: oklch(10% 0.036 29.23); - --alert-L2: oklch(25% 0.103 29.23); - --alert-L3: oklch(37.5% 0.154 29.23); - --alert-L4: oklch(50% 0.195 29.23); - --alert-L5: oklch(62.5% 0.2577 29.23); /* base */ - --alert-L6: oklch(77.2% 0.133 29.23); - --alert-L7: oklch(90% 0.045 29.23); - - --success: var(--success-L6); - --success-L1: oklch(10% 0.029 138.96); - --success-L2: oklch(25% 0.083 138.96); - --success-L3: oklch(37.5% 0.124 138.96); - --success-L4: oklch(50% 0.157 138.96); - --success-L5: oklch(62.5% 0.208 138.96); - --success-L6: oklch(77.2% 0.2607 138.96); /* base */ - --success-L7: oklch(90% 0.201 138.96); - - --neutral: var(--neutral-L2); - --neutral-L1: oklch(10% 0.001 67.66); - --neutral-L2: oklch(25% 0.0026 67.66); /* base */ - --neutral-L3: oklch(37.5% 0.006 67.66); - --neutral-L4: oklch(50% 0.007 67.66); - --neutral-L5: oklch(62.5% 0.009 67.66); - --neutral-L6: oklch(77.2% 0.011 67.66); - --neutral-L7: oklch(90% 0.004 67.66); + --main: oklch(77.2% 0.1738 64.55); + --info: oklch(50% 0.2838 268.01); + --alert: oklch(62.5% 0.2577 29.23); + --success: oklch(77.2% 0.2607 138.96); + --neutral: oklch(25% 0.0026 67.66); + + --bg: color-mix(in oklch, var(--neutral) 20%, white); + --text: color-mix(in oklch, var(--neutral) 20%, black); + --textOnColor: var(--neutral); + @media (prefers-color-scheme: dark) { + --bg: color-mix(in oklch, var(--neutral) 20%, black); + --text: color-mix(in oklch, var(--neutral) 20%, white); + } } @@ -205,6 +175,7 @@ Neutral: #131211 z-index: 100; } #html:target::after { + content: "landscape"; border-color: blue; background: rgba(50, 50, 255, 0.6); left: 14em; @@ -219,13 +190,181 @@ Neutral: #131211 #react-root { display: contents; } + + +/* ============================== + *5. Overrites +================================= */ + +/** MUI OVERWRITES **/ +:root:root, [data-mui-color-scheme="light"][data], [data-mui-color-scheme="dark"][data] { + --mui-palette-common-black: black; + --mui-palette-common-white: white; + --mui-palette-common-background: color-mix(in oklch, var(--neutral) 5%, white); + --mui-palette-common-onBackground: color-mix(in oklch, var(--neutral) 40%, black); + --mui-palette-common-backgroundChannel: 255 255 255; + --mui-palette-common-onBackgroundChannel:0 0 0; + --mui-palette-primary-main: var(--main); + --mui-palette-primary-light: color-mix(in oklch, var(--mui-palette-primary-main) 90%, white); + --mui-palette-primary-dark: color-mix(in oklch, var(--mui-palette-primary-main) 90%, black); + --mui-palette-primary-contrastText: var(--textOnColor); + --mui-palette-primary-mainChannel: 255 125 0; + --mui-palette-primary-lightChannel: var(--mui-palette-primary-mainChannel); + --mui-palette-primary-darkChannel: var(--mui-palette-primary-mainChannel); + --mui-palette-primary-contrastTextChannel: 0 0 0; + --mui-palette-secondary-main: color-mix(in oklch, var(--mui-palette-primary-main) 90%, black); + --mui-palette-secondary-light: color-mix(in oklch, var(--mui-palette-secondary-main) 90%, white); + --mui-palette-secondary-dark: color-mix(in oklch, var(--mui-palette-secondary-main) 90%, black); + --mui-palette-secondary-contrastText: var(--mui-palette-common-onBackground); + --mui-palette-secondary-mainChannel: 240 110 0; + --mui-palette-secondary-lightChannel: var(--mui-palette-secondary-mainChannel); + --mui-palette-secondary-darkChannel: var(--mui-palette-secondary-mainChannel); + --mui-palette-secondary-contrastTextChannel: 255 255 255; + --mui-palette-error-main: var(--alert); + --mui-palette-error-light: color-mix(in oklch, var(--mui-palette-error-main) 90%, white); + --mui-palette-error-dark: color-mix(in oklch, var(--mui-palette-error-main) 90%, black); + --mui-palette-error-contrastText: var(--textOnColor); + --mui-palette-error-mainChannel: 255 47 47; + --mui-palette-error-lightChannel: var(--mui-palette-error-mainChannel); + --mui-palette-error-darkChannel: var(--mui-palette-error-mainChannel); + --mui-palette-error-contrastTextChannel: 0 0 0; + --mui-palette-warning-main: var(--mui-palette-primary-main); + --mui-palette-warning-light: var(--mui-palette-primary-main-light); + --mui-palette-warning-dark: var(--mui-palette-primary-main-dark); + --mui-palette-warning-contrastText: var(--mui-palette-primary-contrastText); + --mui-palette-warning-mainChannel: var(--mui-palette-primary-mainChannel); + --mui-palette-warning-lightChannel: var(--mui-palette-primary-mainChannel); + --mui-palette-warning-darkChannel: var(--mui-palette-primary-mainChannel); + --mui-palette-warning-contrastTextChannel: var(--mui-palette-primary-contrastTextChannel); + --mui-palette-info-main: var(--info); + --mui-palette-info-light: color-mix(in oklch, var(--mui-palette-info-main) 90%, white); + --mui-palette-info-dark: color-mix(in oklch, var(--mui-palette-info-main) 90%, black); + --mui-palette-info-contrastText: var(--textOnColor); + --mui-palette-info-mainChannel: 79 71 240; + --mui-palette-info-lightChannel: var(--mui-palette-info-mainChannel); + --mui-palette-info-darkChannel: var(--mui-palette-info-mainChannel); + --mui-palette-info-contrastTextChannel: 255 255 255; + --mui-palette-success-main: var(--success); + --mui-palette-success-light: color-mix(in oklch, var(--mui-palette-success-main) 90%, white); + --mui-palette-success-dark: color-mix(in oklch, var(--mui-palette-success-main) 90%, black); + --mui-palette-success-contrastText: var(--text); + --mui-palette-success-mainChannel: 122 240 50; + --mui-palette-success-lightChannel: var(--mui-palette-success-mainChannel); + --mui-palette-success-darkChannel: var(--mui-palette-success-mainChannel); + --mui-palette-success-contrastTextChannel: 0 0 0; + --mui-palette-grey-50: #fafafa; + --mui-palette-grey-100:#f5f5f5; + --mui-palette-grey-200:#eeeeee; + --mui-palette-grey-300:#e0e0e0; + --mui-palette-grey-400:#bdbdbd; + --mui-palette-grey-500:#9e9e9e; + --mui-palette-grey-600:#757575; + --mui-palette-grey-700:#616161; + --mui-palette-grey-800:#424242; + --mui-palette-grey-900:#212121; + --mui-palette-grey-A100:#f5f5f5; + --mui-palette-grey-A200:#eeeeee; + --mui-palette-grey-A400:#bdbdbd; + --mui-palette-grey-A700:#616161; + --mui-palette-text-primary: var(--textOnColor); + --mui-palette-text-secondary:var(--mui-palette-text-primary); + --mui-palette-text-disabled: color-mix(in oklch, var(--mui-palette-text-primary) 75%, transparent); + --mui-palette-text-primaryChannel: 0 0 0; + --mui-palette-text-secondaryChannel: var(--mui-palette-text-primaryChannel); + --mui-palette-divider: var(--mui-palette-text-disabled); + --mui-palette-background-paper: var(--mui-palette-common-background); + --mui-palette-background-default: var(--mui-palette-common-background); + --mui-palette-background-defaultChannel: 255 255 255; + --mui-palette-background-paperChannel: 255 255 255; + /* --mui-palette-action-active:rgba(0, 0, 0, 0.54); + --mui-palette-action-hover:rgba(0, 0, 0, 0.04); + --mui-palette-action-hoverOpacity:0.04; + --mui-palette-action-selected:rgba(0, 0, 0, 0.08); + --mui-palette-action-selectedOpacity:0.08; + --mui-palette-action-disabled:rgba(0, 0, 0, 0.26); + --mui-palette-action-disabledBackground:rgba(0, 0, 0, 0.12); + --mui-palette-action-disabledOpacity:0.38; + --mui-palette-action-focus:rgba(0, 0, 0, 0.12); + --mui-palette-action-focusOpacity:0.12; + --mui-palette-action-activatedOpacity:0.12; + --mui-palette-action-activeChannel:0 0 0; + --mui-palette-action-selectedChannel:0 0 0; */ + --mui-palette-Alert-errorColor: var(--mui-palette-error-main); + --mui-palette-Alert-infoColor: var(--mui-palette-info-main); + --mui-palette-Alert-successColor: var(--mui-palette-success-main); + --mui-palette-Alert-warningColor: var(--mui-palette-warning-main); + --mui-palette-Alert-errorFilledBg: var(--mui-palette-error-main); + --mui-palette-Alert-infoFilledBg: var(--mui-palette-info-main); + --mui-palette-Alert-successFilledBg: var(--mui-palette-success-main); + --mui-palette-Alert-warningFilledBg: var(--mui-palette-warning-main); + --mui-palette-Alert-errorFilledColor: var(--mui-palette-error-contrastText); + --mui-palette-Alert-infoFilledColor: var(--mui-palette-info-contrastText); + --mui-palette-Alert-successFilledColor: var(--mui-palette-success-contrastText); + --mui-palette-Alert-warningFilledColor: var(--mui-palette-waring-contrastText); + --mui-palette-Alert-errorStandardBg: var(--mui-palette-common-background); + --mui-palette-Alert-infoStandardBg: var(--mui-palette-common-background); + --mui-palette-Alert-successStandardBg: var(--mui-palette-common-background); + --mui-palette-Alert-warningStandardBg: var(--mui-palette-common-background); + --mui-palette-Alert-errorIconColor: var(--mui-palette-error-main); + --mui-palette-Alert-infoIconColor: var(--mui-palette-info-main); + --mui-palette-Alert-successIconColor: var(--mui-palette-success-main); + --mui-palette-Alert-warningIconColor: var(--mui-palette-warning-main); + --mui-palette-AppBar-defaultBg: var(--mui-palette-grey-100); + --mui-palette-Avatar-defaultBg: var(--mui-palette-grey-400); + --mui-palette-Button-inheritContainedBg: var(--mui-palette-grey-300); + --mui-palette-Button-inheritContainedHoverBg: var(--mui-palette-grey-A100); + --mui-palette-Chip-defaultBorder: var(--mui-palette-grey-400); + --mui-palette-Chip-defaultAvatarColor: var(--mui-palette-grey-700); + --mui-palette-Chip-defaultIconColor: var(--mui-palette-grey-700); + /* --mui-palette-FilledInput-bg:rgba(0, 0, 0, 0.06); + --mui-palette-FilledInput-hoverBg:rgba(0, 0, 0, 0.09); + --mui-palette-FilledInput-disabledBg:rgba(0, 0, 0, 0.12); */ + --mui-palette-LinearProgress-primaryBg: color-mix(in oklch, var(--mui-palette-primary-main) 85%, transparent); + --mui-palette-LinearProgress-secondaryBg: color-mix(in oklch, var(--mui-palette-secondary-main) 85%, transparent); + --mui-palette-LinearProgress-errorBg: color-mix(in oklch, var(--mui-error-primary-main) 85%, transparent); + --mui-palette-LinearProgress-infoBg: color-mix(in oklch, var(--mui-palette-info-main) 85%, transparent); + --mui-palette-LinearProgress-successBg: color-mix(in oklch, var(--mui-palette-success-main) 85%, transparent); + --mui-palette-LinearProgress-warningBg: color-mix(in oklch, var(--mui-palette-warning-main) 85%, transparent); + --mui-palette-Skeleton-bg: rgba(var(--mui-palette-text-primaryChannel) / 0.11); + --mui-palette-Slider-primaryTrack: var(--mui-palette-primary-main); + --mui-palette-Slider-secondaryTrack: var(--mui-palette-secondary-main); + --mui-palette-Slider-errorTrack: var(--mui-error-primary-main); + --mui-palette-Slider-infoTrack: var(--mui-palette-info-main); + --mui-palette-Slider-successTrack: var(--mui-palette-success-main); + --mui-palette-Slider-warningTrack: var(--mui-palette-warning-main); + /* --mui-palette-SnackbarContent-bg:rgb(50, 50, 50); + --mui-palette-SnackbarContent-color:#fff; + --mui-palette-SpeedDialAction-fabHoverBg:rgb(216, 216, 216); */ + --mui-palette-StepConnector-border:var(--mui-palette-grey-400); + --mui-palette-StepContent-border: var(--mui-palette-grey-400); + --mui-palette-Switch-defaultColor: var(--mui-palette-common-white); + --mui-palette-Switch-defaultDisabledColor: var(--mui-palette-grey-100); + --mui-palette-Switch-primaryDisabledColor: color-mix(in oklch, var(--mui-palette-primary-main) 85%, transparent); + --mui-palette-Switch-secondaryDisabledColor: color-mix(in oklch, var(--mui-palette-secondary-main) 85%, transparent); + --mui-palette-Switch-errorDisabledColor: color-mix(in oklch, var(--mui-error-primary-main) 85%, transparent); + --mui-palette-Switch-infoDisabledColor: color-mix(in oklch, var(--mui-palette-info-main) 85%, transparent); + --mui-palette-Switch-successDisabledColor: color-mix(in oklch, var(--mui-palette-success-main) 85%, transparent); + --mui-palette-Switch-warningDisabledColor: color-mix(in oklch, var(--mui-palette-warning-main) 85%, transparent); + /* --mui-palette-TableCell-border:rgba(224, 224, 224, 1); + --mui-palette-Tooltip-bg:rgba(97, 97, 97, 0.92); + --mui-palette-dividerChannel:0 0 0; + --mui-opacity-inputPlaceholder:0.42; + --mui-opacity-inputUnderline:0.42; + --mui-opacity-switchTrackDisabled:0.12; + --mui-opacity-switchTrack:0.38; */ +} + + + + /* ============================== - *5. Media Queries + *6. Media Queries ================================= */ @media (min-width: 30em){#html:target::before {content: ">= 480px"; }} @media (min-width: 48em){#html:target::before {content: ">= 768px"; }} @media (min-width: 64em){#html:target::before {content: ">= 1024px"; }} @media (min-width: 75em){#html:target::before {content: ">= 1200px"; }} -@media (min-width: 100em){#html:target::before {content: ">= 1600px"; }} \ No newline at end of file +@media (min-width: 100em){#html:target::before {content: ">= 1600px"; }} +@media (orientation: portrait) {#html:target::after {content: "portrait"; }} \ No newline at end of file From f46941dfc5f3f58b5295399ceb9682f645685b2a Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 10 May 2024 13:12:02 +0200 Subject: [PATCH 097/206] [Temp] #61 introduce darkmode to MUI --- src/client/index.tsx | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/client/index.tsx b/src/client/index.tsx index 66253da..2e85e6e 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Root, createRoot } from 'react-dom/client'; import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { Experimental_CssVarsProvider as CssVarsProvider, experimental_extendTheme as extendTheme, useColorScheme, getInitColorSchemeScript } from '@mui/material/styles'; import App from "./components/App"; const router = createBrowserRouter([ @@ -10,12 +11,34 @@ const router = createBrowserRouter([ } ]); + +const theme = extendTheme({ // color pallette overwritten in css + typography: { + fontFamily: "Science-Gothic, sans-serif", + fontSize: 20, + }, +}); + +let darkModeEnabled = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; +function setMode(dark:boolean) { + darkModeEnabled = dark; + document.documentElement.dataset.muiColorScheme = darkModeEnabled ? "dark" : "light"; +} + +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { + setMode(event.matches); +}); + + const container = document.getElementById('react-root'); let root: Root; if (container) { + setMode(darkModeEnabled); root = createRoot(container); root.render( - + + + ); } else { console.error("root not found"); From 7df45555b30f31b080b2da77926dd1f6936d87f1 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 10 May 2024 15:48:56 +0200 Subject: [PATCH 098/206] [Task] #61, create new start module so that App can act as root --- src/client/components/Start.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/client/components/Start.tsx diff --git a/src/client/components/Start.tsx b/src/client/components/Start.tsx new file mode 100644 index 0000000..d07972d --- /dev/null +++ b/src/client/components/Start.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { Button, Typography } from '@mui/material'; +import "./css/start.css"; + +function Start() { + return ( +
+

Hello, React!

+ Test Headline + + +
+ ) +} + +export default Start \ No newline at end of file From bbb8421d415ac1a15aa8262f71190f85e05cd5fa Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 10 May 2024 15:49:25 +0200 Subject: [PATCH 099/206] [Task] #61, naming update --- src/client/components/css/{app.css => start.css} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/client/components/css/{app.css => start.css} (98%) diff --git a/src/client/components/css/app.css b/src/client/components/css/start.css similarity index 98% rename from src/client/components/css/app.css rename to src/client/components/css/start.css index ddf6ba5..8c832d1 100644 --- a/src/client/components/css/app.css +++ b/src/client/components/css/start.css @@ -4,7 +4,7 @@ } } -.app { +.start { --bg1: color-mix(in oklch, var(--main) 95%, white); --bg2: var(--main); --text: color-mix(in oklch, var(--neutral) 50%, black);; From 9c9a8c87111c2a7749bf6daf146a6a8467297a6b Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 10 May 2024 15:50:24 +0200 Subject: [PATCH 100/206] [Task] #61, move router to root App --- src/client/components/App.tsx | 23 +++++++++++++++-------- src/client/index.tsx | 27 +++------------------------ 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 3205e16..11878e7 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,16 +1,23 @@ import React from 'react'; -import "./css/app.css"; -import { Button, Typography } from '@mui/material'; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { useColorScheme } from '@mui/material/styles'; +import { useMediaQuery } from '@mui/material'; +import Start from './Start'; +const router = createBrowserRouter([ + { + path: "/", + element: , + } +]); const App = () => { + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + const { mode, setMode } = useColorScheme(); + setMode(prefersDarkMode ? "dark" : "light"); + return ( -
-

Hello, React!

- Test Headline - - -
+ ); } diff --git a/src/client/index.tsx b/src/client/index.tsx index 2e85e6e..0cf03eb 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,43 +1,22 @@ import * as React from 'react'; import { Root, createRoot } from 'react-dom/client'; -import { createBrowserRouter, RouterProvider } from "react-router-dom"; -import { Experimental_CssVarsProvider as CssVarsProvider, experimental_extendTheme as extendTheme, useColorScheme, getInitColorSchemeScript } from '@mui/material/styles'; +import { Experimental_CssVarsProvider as CssVarsProvider, experimental_extendTheme as extendTheme} from '@mui/material/styles'; import App from "./components/App"; -const router = createBrowserRouter([ - { - path: "/", - element: , - } -]); - - const theme = extendTheme({ // color pallette overwritten in css typography: { fontFamily: "Science-Gothic, sans-serif", fontSize: 20, - }, -}); - -let darkModeEnabled = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; -function setMode(dark:boolean) { - darkModeEnabled = dark; - document.documentElement.dataset.muiColorScheme = darkModeEnabled ? "dark" : "light"; -} - -window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { - setMode(event.matches); + } }); - const container = document.getElementById('react-root'); let root: Root; if (container) { - setMode(darkModeEnabled); root = createRoot(container); root.render( - + ); } else { From a5b4ee63232d159aace98050d84ecbdef3c654e4 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 10 May 2024 15:50:47 +0200 Subject: [PATCH 101/206] [Task] #61, add font to preload --- views/index.ejs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/views/index.ejs b/views/index.ejs index c682994..d18fbe2 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -8,6 +8,7 @@ LOREX - Osmand Webtracking Frontend + @@ -19,7 +20,7 @@ - + From 8fad5c5e73ff6c37a6085c7fd174102688b8bf6a Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 14 May 2024 14:16:41 +0200 Subject: [PATCH 102/206] [Task] #61, dim colors in dark mode --- httpdocs/css/base.css | 15 +++++++++++---- views/index.ejs | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index ab89268..0a7df4e 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -123,7 +123,14 @@ Neutral: #131211 --bg: color-mix(in oklch, var(--neutral) 20%, white); --text: color-mix(in oklch, var(--neutral) 20%, black); --textOnColor: var(--neutral); - @media (prefers-color-scheme: dark) { + + /* dark theme, initial state (prefers mq) by react */ + &[data-mui-color-scheme="dark"] { + --main: oklch(75% 0.1738 64.55); + --info: oklch(47.5% 0.2838 268.01); + --alert: oklch(60% 0.2577 29.23); + --success: oklch(75% 0.2607 138.96); + --bg: color-mix(in oklch, var(--neutral) 20%, black); --text: color-mix(in oklch, var(--neutral) 20%, white); } @@ -197,7 +204,7 @@ Neutral: #131211 ================================= */ /** MUI OVERWRITES **/ -:root:root, [data-mui-color-scheme="light"][data], [data-mui-color-scheme="dark"][data] { +:root body { --mui-palette-common-black: black; --mui-palette-common-white: white; --mui-palette-common-background: color-mix(in oklch, var(--neutral) 5%, white); @@ -252,7 +259,7 @@ Neutral: #131211 --mui-palette-success-lightChannel: var(--mui-palette-success-mainChannel); --mui-palette-success-darkChannel: var(--mui-palette-success-mainChannel); --mui-palette-success-contrastTextChannel: 0 0 0; - --mui-palette-grey-50: #fafafa; + /* --mui-palette-grey-50: #fafafa; --mui-palette-grey-100:#f5f5f5; --mui-palette-grey-200:#eeeeee; --mui-palette-grey-300:#e0e0e0; @@ -265,7 +272,7 @@ Neutral: #131211 --mui-palette-grey-A100:#f5f5f5; --mui-palette-grey-A200:#eeeeee; --mui-palette-grey-A400:#bdbdbd; - --mui-palette-grey-A700:#616161; + --mui-palette-grey-A700:#616161; */ --mui-palette-text-primary: var(--textOnColor); --mui-palette-text-secondary:var(--mui-palette-text-primary); --mui-palette-text-disabled: color-mix(in oklch, var(--mui-palette-text-primary) 75%, transparent); diff --git a/views/index.ejs b/views/index.ejs index d18fbe2..dbacecc 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -31,7 +31,7 @@
-

Welcome

+

Lorex

From afdc3b6ae907f6a527592101e6752d7b9f037b4a Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 15 May 2024 13:20:19 +0200 Subject: [PATCH 103/206] [Task] #61, introduce modeswitcher --- src/client/components/ModeSwitcher.tsx | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/client/components/ModeSwitcher.tsx diff --git a/src/client/components/ModeSwitcher.tsx b/src/client/components/ModeSwitcher.tsx new file mode 100644 index 0000000..b86a4e5 --- /dev/null +++ b/src/client/components/ModeSwitcher.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { useColorScheme } from '@mui/material/styles'; +import { Button, useMediaQuery } from '@mui/material'; +import { LightMode, Nightlight } from '@mui/icons-material'; + +function ModeSwitcher() { + const { mode, setMode } = useColorScheme(); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + // run only once + React.useEffect(() => { + setMode(prefersDarkMode ? "dark" : "light"); + }, []); + + return ( + + ); +}; + + +export default ModeSwitcher; From e8bc18c399822e9cafbc3e7ab9bd93ee056171d1 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 15 May 2024 13:21:12 +0200 Subject: [PATCH 104/206] [Change] #64, refactoring splitting pages and components --- src/client/components/App.tsx | 8 ++------ src/client/{components => }/css/start.css | 19 ++++++++++--------- src/client/{components => pages}/Start.tsx | 4 ++-- 3 files changed, 14 insertions(+), 17 deletions(-) rename src/client/{components => }/css/start.css (77%) rename src/client/{components => pages}/Start.tsx (82%) diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 11878e7..1fb1fb6 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,8 +1,6 @@ import React from 'react'; import { createBrowserRouter, RouterProvider } from "react-router-dom"; -import { useColorScheme } from '@mui/material/styles'; -import { useMediaQuery } from '@mui/material'; -import Start from './Start'; +import Start from '../pages/Start'; const router = createBrowserRouter([ { @@ -12,9 +10,7 @@ const router = createBrowserRouter([ ]); const App = () => { - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - const { mode, setMode } = useColorScheme(); - setMode(prefersDarkMode ? "dark" : "light"); + return ( diff --git a/src/client/components/css/start.css b/src/client/css/start.css similarity index 77% rename from src/client/components/css/start.css rename to src/client/css/start.css index 8c832d1..aec0da3 100644 --- a/src/client/components/css/start.css +++ b/src/client/css/start.css @@ -6,9 +6,10 @@ .start { --bg1: color-mix(in oklch, var(--main) 95%, white); - --bg2: var(--main); - --text: color-mix(in oklch, var(--neutral) 50%, black);; - @media (prefers-color-scheme: dark) { + --bg2: var(--main); + --text: color-mix(in oklch, var(--neutral) 50%, black); + + [data-mui-color-scheme="dark"] & { --bg1: color-mix(in oklch, var(--neutral) 90%, black); --bg2: var(--neutral); --text: var(--main); @@ -22,14 +23,14 @@ color: var(--text); background: repeating-linear-gradient(45deg, - var(--bg1), - var(--bg1) 5%, - var(--bg2) 5%, - var(--bg2) 10%); + var(--bg1), + var(--bg1) 5%, + var(--bg2) 5%, + var(--bg2) 10%); background-size: 200px 200px; animation: move-it 10s linear infinite; - + .headline { margin-inline: auto; @@ -43,4 +44,4 @@ display: block; font-size: 2rem; } -} +} \ No newline at end of file diff --git a/src/client/components/Start.tsx b/src/client/pages/Start.tsx similarity index 82% rename from src/client/components/Start.tsx rename to src/client/pages/Start.tsx index d07972d..752f6b4 100644 --- a/src/client/components/Start.tsx +++ b/src/client/pages/Start.tsx @@ -1,11 +1,11 @@ import React from 'react' import { Button, Typography } from '@mui/material'; -import "./css/start.css"; +import "../css/start.css"; function Start() { return (
-

Hello, React!

+

Hello, React!!

Test Headline From 3316aa468371201cb9c2011a0230233e3d91d6c5 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 16 May 2024 17:14:01 +0200 Subject: [PATCH 105/206] [Task] #61, mobile Theme Swticher placed on top right --- src/client/components/ModeSwitcher.tsx | 4 +++- src/client/css/modeSwticher.module.css | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/client/css/modeSwticher.module.css diff --git a/src/client/components/ModeSwitcher.tsx b/src/client/components/ModeSwitcher.tsx index b86a4e5..a9ca0d7 100644 --- a/src/client/components/ModeSwitcher.tsx +++ b/src/client/components/ModeSwitcher.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useColorScheme } from '@mui/material/styles'; import { Button, useMediaQuery } from '@mui/material'; import { LightMode, Nightlight } from '@mui/icons-material'; +import * as css from "../css/modeSwticher.module.css"; function ModeSwitcher() { const { mode, setMode } = useColorScheme(); @@ -13,7 +14,8 @@ function ModeSwitcher() { return ( + +
+ ) +} + +export default Login; \ No newline at end of file From 6879f6526c8c4e415aa0d48abb8e3efcbc8f24ad Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 16 May 2024 17:18:39 +0200 Subject: [PATCH 109/206] [Temp] #61, login controller commented out unused route, TO BE REFACTORED --- src/controller/login.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/controller/login.ts b/src/controller/login.ts index 6a47f4c..b2d9d10 100644 --- a/src/controller/login.ts +++ b/src/controller/login.ts @@ -7,13 +7,14 @@ import { createJWT, createCSRF, validateCSRF } from '@src/scripts/token'; const router = express.Router(); -router.get("/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response, next: NextFunction) { - loginLimiter(req, res, () => { - const csrfToken = createCSRF(res, next); - res.locals = {...res.locals, text: 'start', csrfToken: csrfToken}; - res.render("login-form"); - }); -}); +// TODO refactor endpoint to get token +// router.get("/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response, next: NextFunction) { +// loginLimiter(req, res, () => { +// const csrfToken = createCSRF(res, next); +// res.locals = {...res.locals, text: 'start', csrfToken: csrfToken}; +// res.render("login-form"); +// }); +// }); router.post("/", loginSlowDown, async function postLogin(req: Request, res: Response, next: NextFunction) { loginLimiter(req, res, async () => { From 2a64701b44a97ac3c751c858ad0513da05e0b9df Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 17 May 2024 00:38:08 +0200 Subject: [PATCH 110/206] [Task] #63, login validation --- src/client/css/login.css | 6 ++++- src/client/pages/Login.tsx | 54 +++++++++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/client/css/login.css b/src/client/css/login.css index a22a3e6..f2b1e69 100644 --- a/src/client/css/login.css +++ b/src/client/css/login.css @@ -70,12 +70,16 @@ max-height: 100%; height: 4.15em; } + + p { + margin-top: 0.5em; + } } .submit { align-self: end; - [data-mui-color-scheme="light"] & { + [data-mui-color-scheme="light"] &:not([disabled]) { color: var(--main); background-color: color-mix(in oklch, var(--neutral) 90%, transparent); } diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index e94d8c1..b265b3c 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { TextField, Button, InputAdornment } from '@mui/material'; import { AccountCircle, Lock } from '@mui/icons-material'; import "../css/login.css"; @@ -10,9 +10,41 @@ function submit(e) { } function Login() { + const [formData, updateFormData] = useState({ + user: { + isError: false, + message: "Minimum 2", + value: "" + }, + password: { + isError: false, + message: "Enter Password", + value: "" + }, + token: "" + }); + + const isFormValid = formData.user.value !== '' && formData.password.value !== ''; //&& formData.token; + + function updateField(name:string, value:string) { + const hasError = validateField(name, value, false); + const newObj = { ...formData, [name] : {...formData[name], value: value }} + if (!hasError) {newObj[name].isError = false} // remove error state while typing but don't add before blur + updateFormData(newObj) + } + + function validateField(name:string, value:string, update = true) { + const isError = value.length <= 1; + if (update) { + updateFormData({ ...formData, [name] : {...formData[name], isError: isError}}) + } else { + return isError; + } + } + return (
- +

Login Page

@@ -20,8 +52,15 @@ function Login() { updateField(e.target.name, e.target.value)} + onBlur={(e) => validateField(e.target.name, e.target.value)} + error={formData.user.isError} + helperText={formData.user.isError ? formData.user.message : false} + required InputProps={{ autoFocus: true, + name: "user", startAdornment: ( @@ -33,7 +72,14 @@ function Login() { label="Password" type="password" variant="filled" + value={formData.password.value} + onChange={(e) => updateField(e.target.name, e.target.value)} + onBlur={(e) => validateField(e.target.name, e.target.value)} + required + error={formData.password.isError} + helperText={formData.password.isError ? formData.password.message : false} InputProps={{ + name: "password", startAdornment: ( @@ -41,11 +87,13 @@ function Login() { ), }} /> + updateFormData({ ...formData, [e.target.name]: e.target.value })} type="hidden" id="csrfToken" value={formData.token} name="csrfToken" /> @@ -54,4 +102,4 @@ function Login() { ) } -export default Login; \ No newline at end of file +export default Login; From 97e93ece8a5f25e0e6164b8002170f1ab8c6c393 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 17 May 2024 00:52:51 +0200 Subject: [PATCH 111/206] [Task] #63, add error icon --- src/client/pages/Login.tsx | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index b265b3c..fe2ee3e 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { TextField, Button, InputAdornment } from '@mui/material'; -import { AccountCircle, Lock } from '@mui/icons-material'; +import { AccountCircle, Lock, HighlightOff } from '@mui/icons-material'; import "../css/login.css"; import ModeSwitcher from '../components/ModeSwitcher'; @@ -24,19 +24,20 @@ function Login() { token: "" }); - const isFormValid = formData.user.value !== '' && formData.password.value !== ''; //&& formData.token; + const isFormValid = formData.user.value && !formData.user.isError && formData.password.value && !formData.password.isError; //&& formData.token; - function updateField(name:string, value:string) { + function updateField(name: string, value: string) { const hasError = validateField(name, value, false); - const newObj = { ...formData, [name] : {...formData[name], value: value }} - if (!hasError) {newObj[name].isError = false} // remove error state while typing but don't add before blur + console.log(hasError); + const newObj = { ...formData, [name]: { ...formData[name], value: value } } + if (!hasError) { newObj[name].isError = false } // remove error state while typing but don't add before blur event updateFormData(newObj) } - function validateField(name:string, value:string, update = true) { + function validateField(name: string, value: string, update = true) { const isError = value.length <= 1; if (update) { - updateFormData({ ...formData, [name] : {...formData[name], isError: isError}}) + updateFormData({ ...formData, [name]: { ...formData[name], isError: isError } }) } else { return isError; } @@ -66,8 +67,14 @@ function Login() { ), + endAdornment: formData.user.isError ? ( + + + + ) : null }} /> + ), + endAdornment: formData.password.isError ? ( + + + + ) : null }} /> updateFormData({ ...formData, [e.target.name]: e.target.value })} type="hidden" id="csrfToken" value={formData.token} name="csrfToken" /> From a012ed9233c004d3b97bf89b6dc4abc2a48ea902 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 21 May 2024 12:48:41 +0200 Subject: [PATCH 112/206] [Task] #61, cut design update --- httpdocs/css/base.css | 7 +++++++ src/client/css/login.css | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index 5bc9da9..a1274ba 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -123,6 +123,7 @@ Neutral: #131211 --bg: color-mix(in oklch, var(--neutral) 20%, white); --text: color-mix(in oklch, var(--neutral) 20%, black); --textOnColor: var(--neutral); + --semiBg: #ffffffbb; /* dark theme, initial state (prefers mq) by react */ &[data-mui-color-scheme="dark"] { @@ -133,6 +134,7 @@ Neutral: #131211 --bg: color-mix(in oklch, var(--neutral) 20%, black); --text: color-mix(in oklch, var(--neutral) 20%, white); + --semiBg: #00000077; } } @@ -151,6 +153,11 @@ Neutral: #131211 position:absolute; width:1px } +.cut { + --cut: 2em; + clip-path: polygon(0% var(--cut), var(--cut) 0%, 100% 0, 100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%); +} + @media screen and (prefers-reduced-motion: reduce), diff --git a/src/client/css/login.css b/src/client/css/login.css index f2b1e69..b7d893c 100644 --- a/src/client/css/login.css +++ b/src/client/css/login.css @@ -31,6 +31,14 @@ animation: move-it 10s linear infinite; + .wrapper { + max-width: 50em; + padding: 2.5em; + margin: 2.5em; + background: var(--semiBg); + --cut: 3em; + .cut { --cut: 1.1em } /* reset for child elements */ + } .headline { margin-inline: auto; @@ -43,7 +51,7 @@ form { display: flex; flex-flow: nowrap column; - gap: 1.5em; + gap: 2em; margin-bottom: 10vh; } @@ -78,13 +86,16 @@ .submit { align-self: end; + font-size: 1.325rem; + min-width: 9em; + min-height: 3.5em; + border-radius: 0; [data-mui-color-scheme="light"] &:not([disabled]) { color: var(--main); background-color: color-mix(in oklch, var(--neutral) 90%, transparent); } - } From c505d2b314abec9f944e4ddaf7c9addf7156721e Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 21 May 2024 12:50:40 +0200 Subject: [PATCH 113/206] [Task] #53, apply cut, rename FormData to FormInfo to avoid confusion with reserved name --- src/client/pages/Login.tsx | 170 +++++++++++++++++++++---------------- 1 file changed, 98 insertions(+), 72 deletions(-) diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index fe2ee3e..11821ca 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -4,13 +4,10 @@ import { AccountCircle, Lock, HighlightOff } from '@mui/icons-material'; import "../css/login.css"; import ModeSwitcher from '../components/ModeSwitcher'; -function submit(e) { - e.preventDefault(); - console.log("submit"); -} + function Login() { - const [formData, updateFormData] = useState({ + const [formInfo, updateFormInfo] = useState({ user: { isError: false, message: "Minimum 2", @@ -24,92 +21,121 @@ function Login() { token: "" }); - const isFormValid = formData.user.value && !formData.user.isError && formData.password.value && !formData.password.isError; //&& formData.token; + const isFormValid = formInfo.user.value && !formInfo.user.isError && formInfo.password.value && !formInfo.password.isError; //&& formInfo.token; function updateField(name: string, value: string) { const hasError = validateField(name, value, false); console.log(hasError); - const newObj = { ...formData, [name]: { ...formData[name], value: value } } + const newObj = { ...formInfo, [name]: { ...formInfo[name], value: value } } if (!hasError) { newObj[name].isError = false } // remove error state while typing but don't add before blur event - updateFormData(newObj) + updateFormInfo(newObj) } function validateField(name: string, value: string, update = true) { const isError = value.length <= 1; if (update) { - updateFormData({ ...formData, [name]: { ...formData[name], isError: isError } }) + updateFormInfo({ ...formInfo, [name]: { ...formInfo[name], isError: isError } }) } else { return isError; } } + async function submit(e) { + e.preventDefault(); + + const formData = new FormData(); + formData.append('user', formInfo.user.value); + formData.append('password', formInfo.password.value); + formData.append('token', formInfo.token); + + try { + const response = await fetch("/login", { + method: "POST", + body: formData, + }); + console.log(response); + } catch (error) { + console.log(error); + } + } + return (
-

- Login Page -

-
- updateField(e.target.name, e.target.value)} - onBlur={(e) => validateField(e.target.name, e.target.value)} - error={formData.user.isError} - helperText={formData.user.isError ? formData.user.message : false} - required - InputProps={{ - autoFocus: true, - name: "user", - startAdornment: ( - - - - ), - endAdornment: formData.user.isError ? ( - - - - ) : null - }} - /> +
+ + +

+ Login Page +

+ + updateField(e.target.name, e.target.value)} + onBlur={(e) => validateField(e.target.name, e.target.value)} + error={formInfo.user.isError} + helperText={formInfo.user.isError ? formInfo.user.message : false} + required + InputProps={{ + classes: { + root: "cut", + }, + autoFocus: true, + name: "user", + startAdornment: ( + + + + ), + endAdornment: formInfo.user.isError ? ( + + + + ) : null + }} + /> - updateField(e.target.name, e.target.value)} - onBlur={(e) => validateField(e.target.name, e.target.value)} - required - error={formData.password.isError} - helperText={formData.password.isError ? formData.password.message : false} - InputProps={{ - name: "password", - startAdornment: ( - - - - ), - endAdornment: formData.password.isError ? ( - - - - ) : null - }} - /> - updateFormData({ ...formData, [e.target.name]: e.target.value })} type="hidden" id="csrfToken" value={formData.token} name="csrfToken" /> - - + updateField(e.target.name, e.target.value)} + onBlur={(e) => validateField(e.target.name, e.target.value)} + required + error={formInfo.password.isError} + helperText={formInfo.password.isError ? formInfo.password.message : false} + InputProps={{ + classes: { + root: "cut", + }, + name: "password", + startAdornment: ( + + + + ), + endAdornment: formInfo.password.isError ? ( + + + + ) : null + }} + /> + updateFormInfo({ ...formInfo, [e.target.name]: e.target.value })} type="hidden" id="csrfToken" value={formInfo.token} name="csrfToken" /> + + +
) } From 8e0bb6b4a322e77ce59a8d90d764f741c5209966 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 21 May 2024 16:46:18 +0200 Subject: [PATCH 114/206] [Task] #63, send login request --- package-lock.json | 21 +++++++-------------- package.json | 2 +- src/client/pages/Login.tsx | 23 ++++++++++------------- src/controller/login.ts | 1 + 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e7c66e..58deecc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.16", "@mui/material": "^5.15.16", + "axios": "^1.7.1", "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", @@ -45,7 +46,6 @@ "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", - "axios": "^1.6.5", "concurrently": "^8.2.2", "css-loader": "^7.1.0", "eslint": "^8.56.0", @@ -3252,8 +3252,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/available-typed-arrays": { "version": "1.0.7", @@ -3280,12 +3279,11 @@ } }, "node_modules/axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", - "dev": true, + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.1.tgz", + "integrity": "sha512-+LV37nQcd1EpFalkXksWNBiA17NZ5m5/WspmHGmZmdx1qBOg/VNq/c4eRJiA9VQQHBOs+N0ZhhdU10h2TyNK7Q==", "dependencies": { - "follow-redirects": "^1.15.4", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -3796,7 +3794,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4205,7 +4202,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -5530,7 +5526,6 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, "funding": [ { "type": "individual", @@ -5559,7 +5554,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -11355,8 +11349,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/pstree.remy": { "version": "1.1.8", diff --git a/package.json b/package.json index a673eba..d1e263c 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", - "axios": "^1.6.5", "concurrently": "^8.2.2", "css-loader": "^7.1.0", "eslint": "^8.56.0", @@ -65,6 +64,7 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.16", "@mui/material": "^5.15.16", + "axios": "^1.7.1", "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index 11821ca..7b4c70d 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -3,7 +3,8 @@ import { TextField, Button, InputAdornment } from '@mui/material'; import { AccountCircle, Lock, HighlightOff } from '@mui/icons-material'; import "../css/login.css"; import ModeSwitcher from '../components/ModeSwitcher'; - +import axios from 'axios'; +import qs from 'qs'; function Login() { @@ -25,7 +26,6 @@ function Login() { function updateField(name: string, value: string) { const hasError = validateField(name, value, false); - console.log(hasError); const newObj = { ...formInfo, [name]: { ...formInfo[name], value: value } } if (!hasError) { newObj[name].isError = false } // remove error state while typing but don't add before blur event updateFormInfo(newObj) @@ -43,16 +43,15 @@ function Login() { async function submit(e) { e.preventDefault(); - const formData = new FormData(); - formData.append('user', formInfo.user.value); - formData.append('password', formInfo.password.value); - formData.append('token', formInfo.token); - + const bodyFormData = { "user": formInfo.user.value, "password": formInfo.password.value }; try { - const response = await fetch("/login", { - method: "POST", - body: formData, - }); + const response = await axios({ + method: "post", + url: "/login", + data: qs.stringify(bodyFormData), + headers: { "content-type": "application/x-www-form-urlencoded" } + }) + console.log(response); } catch (error) { console.log(error); @@ -63,8 +62,6 @@ function Login() {
- -

Login Page

diff --git a/src/controller/login.ts b/src/controller/login.ts index b2d9d10..6c855d4 100644 --- a/src/controller/login.ts +++ b/src/controller/login.ts @@ -23,6 +23,7 @@ router.post("/", loginSlowDown, async function postLogin(req: Request, res: Resp const user = req.body.user; const password = req.body.password; let userFound = false; + console.log(`user: ${user}, password: ${password}`); if (!user || !password) { return createError(res, 422, "Body does not contain all expected information", next); } if (!token || !validateCSRF(req.body.csrfToken)) { return createError(res, 403, "Invalid CSRF Token", next); } From 10646b6831397293d925646c461b39c3a61876fb Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 21 May 2024 20:53:41 +0200 Subject: [PATCH 115/206] [Task] #61, loading icon --- src/client/css/login.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/client/css/login.css b/src/client/css/login.css index b7d893c..cc4ffe0 100644 --- a/src/client/css/login.css +++ b/src/client/css/login.css @@ -86,7 +86,7 @@ .submit { align-self: end; - font-size: 1.325rem; + font-size: 1.5rem; min-width: 9em; min-height: 3.5em; border-radius: 0; @@ -96,6 +96,10 @@ background-color: color-mix(in oklch, var(--neutral) 90%, transparent); } + .MuiButton-icon { + font-size: 0.8em; + } + } From 59b31e07a9f2faa42b982a29e6817f1dad7355b8 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 21 May 2024 21:20:01 +0200 Subject: [PATCH 116/206] [Task] #63, get csrfToken, fullfill login request --- src/client/pages/Login.tsx | 24 +++++++++++++++++++----- src/controller/login.ts | 13 ++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index 7b4c70d..44ae404 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { TextField, Button, InputAdornment } from '@mui/material'; -import { AccountCircle, Lock, HighlightOff } from '@mui/icons-material'; +import { TextField, Button, InputAdornment, CircularProgress } from '@mui/material'; +import { AccountCircle, Lock, HighlightOff, Login as LoginIcon } from '@mui/icons-material'; import "../css/login.css"; import ModeSwitcher from '../components/ModeSwitcher'; import axios from 'axios'; @@ -21,6 +21,7 @@ function Login() { }, token: "" }); + const [isLoading, setLoading] = React.useState(false); const isFormValid = formInfo.user.value && !formInfo.user.isError && formInfo.password.value && !formInfo.password.isError; //&& formInfo.token; @@ -42,8 +43,18 @@ function Login() { async function submit(e) { e.preventDefault(); + setLoading(true); - const bodyFormData = { "user": formInfo.user.value, "password": formInfo.password.value }; + let token = null; // get csrf token + try { + token = (await axios.get("/login/csrf")).data; + updateFormInfo({ ...formInfo, token: token }) + } catch (error) { + console.log(error); + } + + // collect data and convert to urlencoded string then send + const bodyFormData = { "user": formInfo.user.value, "password": formInfo.password.value, csrfToken: token}; try { const response = await axios({ method: "post", @@ -55,6 +66,8 @@ function Login() { console.log(response); } catch (error) { console.log(error); + } finally { + setLoading(false); // Reset loading after request is complete } } @@ -121,13 +134,14 @@ function Login() { ) : null }} /> - updateFormInfo({ ...formInfo, [e.target.name]: e.target.value })} type="hidden" id="csrfToken" value={formInfo.token} name="csrfToken" /> + diff --git a/src/controller/login.ts b/src/controller/login.ts index 6c855d4..5d369f1 100644 --- a/src/controller/login.ts +++ b/src/controller/login.ts @@ -8,13 +8,12 @@ import { createJWT, createCSRF, validateCSRF } from '@src/scripts/token'; const router = express.Router(); // TODO refactor endpoint to get token -// router.get("/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response, next: NextFunction) { -// loginLimiter(req, res, () => { -// const csrfToken = createCSRF(res, next); -// res.locals = {...res.locals, text: 'start', csrfToken: csrfToken}; -// res.render("login-form"); -// }); -// }); +router.get("/csrf", baseSlowDown, baseRateLimiter, async function csrf(req: Request, res: Response, next: NextFunction) { + loginLimiter(req, res, () => { + const csrfToken = createCSRF(res, next); + res.json(csrfToken); + }); +}); router.post("/", loginSlowDown, async function postLogin(req: Request, res: Response, next: NextFunction) { loginLimiter(req, res, async () => { From 5bc4e04bdf74968a4b9bd1a90f7c8b1b80a11231 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 22 May 2024 13:02:04 +0200 Subject: [PATCH 117/206] [Fix] #63, fail gracefully when too many tokens --- src/controller/login.ts | 6 ++++-- src/scripts/token.ts | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/controller/login.ts b/src/controller/login.ts index 5d369f1..216319b 100644 --- a/src/controller/login.ts +++ b/src/controller/login.ts @@ -11,7 +11,9 @@ const router = express.Router(); router.get("/csrf", baseSlowDown, baseRateLimiter, async function csrf(req: Request, res: Response, next: NextFunction) { loginLimiter(req, res, () => { const csrfToken = createCSRF(res, next); - res.json(csrfToken); + if (csrfToken) { + res.json(csrfToken); + } }); }); @@ -24,7 +26,7 @@ router.post("/", loginSlowDown, async function postLogin(req: Request, res: Resp let userFound = false; console.log(`user: ${user}, password: ${password}`); if (!user || !password) { return createError(res, 422, "Body does not contain all expected information", next); } - if (!token || !validateCSRF(req.body.csrfToken)) { return createError(res, 403, "Invalid CSRF Token", next); } + if (!token || !validateCSRF(req.body.csrfToken)) { return createError(res, 403, "Invalid CSRF Token \n retry in 5 Minuits", next); } // Loop through all environment variables for (const key in process.env) { diff --git a/src/scripts/token.ts b/src/scripts/token.ts index f9f4d7a..78fb959 100644 --- a/src/scripts/token.ts +++ b/src/scripts/token.ts @@ -6,10 +6,11 @@ import { create as createError } from '@src/middleware/error'; const csrfTokens: Set = new Set(); -export function createCSRF(res: Response, next: NextFunction): string { +export function createCSRF(res: Response, next: NextFunction): string | false { if (csrfTokens.size > 100) { // Max Number of Tokens in memory res.set('Retry-After', '300'); // 5 minutes - createError(res, 503, "Too many tokens", next); + createError(res, 503, "Too many tokens \n retry after 5 Minuits", next); + return false; } const token = crypto.randomBytes(16).toString('hex'); From ace6b5a81ea9c79e695ff3847942a0a2fd98f462 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 22 May 2024 13:02:30 +0200 Subject: [PATCH 118/206] [Task] #63, error Handling in login form --- src/client/css/login.css | 16 +++++++++++- src/client/pages/Login.tsx | 50 ++++++++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/client/css/login.css b/src/client/css/login.css index cc4ffe0..a764b90 100644 --- a/src/client/css/login.css +++ b/src/client/css/login.css @@ -84,9 +84,15 @@ } } + .subWrapper { + display: flex; + width: 100%; + align-items: center; + } + .submit { - align-self: end; font-size: 1.5rem; + margin-left: auto; min-width: 9em; min-height: 3.5em; border-radius: 0; @@ -99,7 +105,15 @@ .MuiButton-icon { font-size: 0.8em; } + } + + .errorMessage { + color: var(--alert); + font-size: 1.8em; + font-style: italics; + margin: 0; + strong { margin-right: 0.3em;} } diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index 44ae404..ddfaac8 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -5,6 +5,7 @@ import "../css/login.css"; import ModeSwitcher from '../components/ModeSwitcher'; import axios from 'axios'; import qs from 'qs'; +import { error } from 'console'; function Login() { @@ -22,6 +23,7 @@ function Login() { token: "" }); const [isLoading, setLoading] = React.useState(false); + const [errorObj, setErrorObj] = React.useState({ status: null, message: null }); const isFormValid = formInfo.user.value && !formInfo.user.isError && formInfo.password.value && !formInfo.password.isError; //&& formInfo.token; @@ -44,17 +46,22 @@ function Login() { async function submit(e) { e.preventDefault(); setLoading(true); + setErrorObj({ status: null, message: null }) + let token = null; // get csrf token try { - token = (await axios.get("/login/csrf")).data; - updateFormInfo({ ...formInfo, token: token }) + token = await axios.get("/login/csrf"); + updateFormInfo({ ...formInfo, token: token.data }); } catch (error) { + setErrorObj({ status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) console.log(error); } + if (!token) {setLoading(false); return; } // skip when the first request has an error + // collect data and convert to urlencoded string then send - const bodyFormData = { "user": formInfo.user.value, "password": formInfo.password.value, csrfToken: token}; + const bodyFormData = { "user": formInfo.user.value, "password": formInfo.password.value, csrfToken: token.data }; try { const response = await axios({ method: "post", @@ -62,9 +69,8 @@ function Login() { data: qs.stringify(bodyFormData), headers: { "content-type": "application/x-www-form-urlencoded" } }) - - console.log(response); } catch (error) { + setErrorObj({ status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) console.log(error); } finally { setLoading(false); // Reset loading after request is complete @@ -135,16 +141,30 @@ function Login() { }} /> - +
+ {errorObj.status ? ( +

+ {errorObj.status} + {errorObj.message.split('\n').map((line:string, index:string) => ( + + {line} +
+
+ ))} +

+ ) : null} + +
+
From ffd81834cae8a5ebdc9db3a28f30100b87672e90 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 27 May 2024 11:55:15 +0200 Subject: [PATCH 119/206] [Task] #81, remove password log --- src/controller/login.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controller/login.ts b/src/controller/login.ts index 216319b..c6bf4b6 100644 --- a/src/controller/login.ts +++ b/src/controller/login.ts @@ -24,7 +24,6 @@ router.post("/", loginSlowDown, async function postLogin(req: Request, res: Resp const user = req.body.user; const password = req.body.password; let userFound = false; - console.log(`user: ${user}, password: ${password}`); if (!user || !password) { return createError(res, 422, "Body does not contain all expected information", next); } if (!token || !validateCSRF(req.body.csrfToken)) { return createError(res, 403, "Invalid CSRF Token \n retry in 5 Minuits", next); } From 9e5781a4a8f6fd7670b970023bb8a0e7b2f874a6 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 27 May 2024 11:56:10 +0200 Subject: [PATCH 120/206] [Task] #80, cleanup todo token --- src/controller/login.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controller/login.ts b/src/controller/login.ts index c6bf4b6..6c108f8 100644 --- a/src/controller/login.ts +++ b/src/controller/login.ts @@ -7,7 +7,6 @@ import { createJWT, createCSRF, validateCSRF } from '@src/scripts/token'; const router = express.Router(); -// TODO refactor endpoint to get token router.get("/csrf", baseSlowDown, baseRateLimiter, async function csrf(req: Request, res: Response, next: NextFunction) { loginLimiter(req, res, () => { const csrfToken = createCSRF(res, next); From 980317eb97758bb0e1c81c40af8d7e60fa6d9832 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 27 May 2024 11:59:34 +0200 Subject: [PATCH 121/206] fix: upgrade multiple dependencies with Snyk (#68) Snyk has created this PR to upgrade: - react from 18.2.0 to 18.3.1. See this package in npm: https://www.npmjs.com/package/react - react-dom from 18.2.0 to 18.3.1. See this package in npm: https://www.npmjs.com/package/react-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 29 ++++++++++++++++------------- package.json | 4 ++-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58deecc..be24e5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,8 +26,8 @@ "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", "module-alias": "^2.2.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-router-dom": "^6.23.0", "toobusy-js": "^0.5.1" }, @@ -11448,9 +11448,10 @@ } }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -11459,15 +11460,16 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-is": { @@ -11786,9 +11788,10 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } diff --git a/package.json b/package.json index d1e263c..e089f76 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", "module-alias": "^2.2.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-router-dom": "^6.23.0", "toobusy-js": "^0.5.1" }, From 86babe8310dfba31c1545cc99476c221702e3b11 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 27 May 2024 12:35:03 +0200 Subject: [PATCH 122/206] [Fix] #64, disable express header --- src/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app.ts b/src/app.ts index a9c323d..92bc792 100644 --- a/src/app.ts +++ b/src/app.ts @@ -19,6 +19,7 @@ import { cleanupCSRF } from "@src/scripts/token"; config(); // dotenv const app = express(); +app.disable("x-powered-by"); app.set('view engine', 'ejs'); From f9aa2fc5fb741b4a418f74e9813fde1e85360868 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 27 May 2024 12:35:59 +0200 Subject: [PATCH 123/206] [Task] #64, protect csrf token page with custom http header --- src/client/pages/Login.tsx | 9 ++++++++- src/controller/login.ts | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index ddfaac8..f688863 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -51,7 +51,14 @@ function Login() { let token = null; // get csrf token try { - token = await axios.get("/login/csrf"); + token = await axios({ + method: "post", + url: "/login/csrf", + headers: { + "content-type": "application/x-www-form-urlencoded", + "x-requested-with": "XMLHttpRequest" + } + }) updateFormInfo({ ...formInfo, token: token.data }); } catch (error) { setErrorObj({ status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) diff --git a/src/controller/login.ts b/src/controller/login.ts index 6c108f8..6eaa313 100644 --- a/src/controller/login.ts +++ b/src/controller/login.ts @@ -7,8 +7,11 @@ import { createJWT, createCSRF, validateCSRF } from '@src/scripts/token'; const router = express.Router(); -router.get("/csrf", baseSlowDown, baseRateLimiter, async function csrf(req: Request, res: Response, next: NextFunction) { +router.post("/csrf", baseSlowDown, baseRateLimiter, async function csrf(req: Request, res: Response, next: NextFunction) { loginLimiter(req, res, () => { + if (req.headers['x-requested-with'] !== 'XMLHttpRequest') { + return createError(res, 403, "Unable to provide token", next); + } const csrfToken = createCSRF(res, next); if (csrfToken) { res.json(csrfToken); From d3e19f12787d8b398e1a2f38f3e7d2618420755f Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 27 May 2024 14:09:30 +0200 Subject: [PATCH 124/206] [FIx] #64, fix csrf test --- src/tests/login.test.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/tests/login.test.ts b/src/tests/login.test.ts index e177826..4bb5699 100644 --- a/src/tests/login.test.ts +++ b/src/tests/login.test.ts @@ -19,22 +19,30 @@ const userDataWithToken = { }; describe('Login', () => { - it('form available', async () => { + it('csrf available', async () => { let serverStatus = {}; let response = { data: "", status: "" }; try { - response = await axios.get('http://localhost:80/login'); + response = await axios({ + method: "post", + url: "/login/csrf", + headers: { + "content-type": "application/x-www-form-urlencoded", + "x-requested-with": "XMLHttpRequest" + } + }) serverStatus = response.status; } catch (error) { console.error(error); } + expect(serverStatus).toBe(200); - expect(response.data).toContain(' { From 3f92443de94d06f751561e4c55174d0503d74f64 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 28 May 2024 13:47:01 +0200 Subject: [PATCH 125/206] [Task] #64, repair test cases --- src/tests/integration.test.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index 067d3ea..2696d06 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -237,17 +237,23 @@ describe('read and login', () => { csrfToken: "" } - it('form available / get Token', async () => { + it('get csrfToken', async () => { let response = {data:""}; try { - response = await axios.get('http://localhost:80/login'); + response = await axios({ + method: "post", + url: "http://localhost/login/csrf", + headers: { + "content-type": "application/x-www-form-urlencoded", + "x-requested-with": "XMLHttpRequest" + } + }) } catch (error) { console.error(error); } - - const regex = /name="csrfToken" value="([^"]*)"/; - const match = response.data.match(regex); - testData.csrfToken = match ? match[1] : '-'; + + testData.csrfToken = response.data; + expect(testData.csrfToken).toBeTruthy(); }) test(`redirect without logged in`, async () => { From 1f080db0a21b9435e849ccd657fe7ef95cc401b5 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 28 May 2024 15:15:12 +0200 Subject: [PATCH 126/206] fix: upgrade express-slow-down from 2.0.1 to 2.0.2 (#69) Snyk has created this PR to upgrade express-slow-down from 2.0.1 to 2.0.2. See this package in npm: express-slow-down See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 11 ++++++----- package.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index be24e5f..08cc87d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "ejs": "^3.1.10", "express": "^4.19.2", "express-rate-limit": "^7.2.0", - "express-slow-down": "^2.0.1", + "express-slow-down": "^2.0.2", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", @@ -5318,9 +5318,10 @@ } }, "node_modules/express-slow-down": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/express-slow-down/-/express-slow-down-2.0.1.tgz", - "integrity": "sha512-zRogSZhNXJYKDBekhgFfFXGrOngH7Fub7Mx2g8OQ4RUBwSJP/3TVEKMgSGR/WlneT0mJ6NBUnidHhIELGVPe3w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/express-slow-down/-/express-slow-down-2.0.2.tgz", + "integrity": "sha512-Ql2gg6bBFVzwGUFLOPRcy8JkAMEinLZcdUpClIzsAizWh7AFvq93NHx7naugl73e/UoChncKuw5hQURBeSYtVg==", + "license": "MIT", "dependencies": { "express-rate-limit": "7" }, @@ -5328,7 +5329,7 @@ "node": ">= 16" }, "peerDependencies": { - "express": ">= 4" + "express": "^4.0.0 || ^5.0.0" } }, "node_modules/express-validator": { diff --git a/package.json b/package.json index e089f76..3132a24 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "ejs": "^3.1.10", "express": "^4.19.2", "express-rate-limit": "^7.2.0", - "express-slow-down": "^2.0.1", + "express-slow-down": "^2.0.2", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", From ae7e36ea52b1c640acafb089d17c83c27fe26ce3 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 28 May 2024 15:18:53 +0200 Subject: [PATCH 127/206] [fix] #64, linter fixes --- src/client/pages/Login.tsx | 5 ++--- src/tests/app.test.ts | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index f688863..df8eb4d 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -5,8 +5,6 @@ import "../css/login.css"; import ModeSwitcher from '../components/ModeSwitcher'; import axios from 'axios'; import qs from 'qs'; -import { error } from 'console'; - function Login() { const [formInfo, updateFormInfo] = useState({ @@ -70,7 +68,8 @@ function Login() { // collect data and convert to urlencoded string then send const bodyFormData = { "user": formInfo.user.value, "password": formInfo.password.value, csrfToken: token.data }; try { - const response = await axios({ + // nextup store token for further usuage + await axios({ method: "post", url: "/login", data: qs.stringify(bodyFormData), diff --git a/src/tests/app.test.ts b/src/tests/app.test.ts index 44ba08d..ee74eb6 100644 --- a/src/tests/app.test.ts +++ b/src/tests/app.test.ts @@ -9,8 +9,24 @@ const randomData = qs.stringify({ describe('Server Status', () => { it('The server is running', async () => { let serverStatus; + let response; try { - const response = await axios.get('http://localhost:80/'); + response = await axios.get('http://localhost:80/'); + serverStatus = response.status; + } catch (error) { + console.error(error); + return; + } + + expect(serverStatus).toBe(200); + expect(response.data).toContain("js/bundle.js"); + }) + + it('bundle.js exists', async () => { + let serverStatus; + let response; + try { + response = await axios.get('http://localhost:80/js/bundle.js'); serverStatus = response.status; } catch (error) { console.error(error); From 49947f892c8e71acd7c32e9d6c6ee16e920d0e91 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 29 May 2024 01:57:28 +0200 Subject: [PATCH 128/206] [Task] Editor Config --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4a44c2b..3311857 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "workbench.editor.enablePreview": false, "editor.rename.enablePreview": false, - "typescript.tsdk": "node_modules\\typescript\\lib" + "typescript.tsdk": "node_modules\\typescript\\lib", + "html.format.wrapLineLength": 150 } \ No newline at end of file From d6468b19da176c92c5e3c1fc6f38967b5c8bdb02 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 29 May 2024 01:58:15 +0200 Subject: [PATCH 129/206] [Task] #61, convert background line to svg and animate --- httpdocs/css/base.css | 33 +++++++++++++++++++++++++++++-- src/client/css/login.css | 36 ++++++++++++++++++++-------------- src/client/css/start.css | 13 ------------- src/client/index.tsx | 2 +- src/client/pages/Login.tsx | 4 +++- src/client/pages/Start.tsx | 10 +++++++--- views/index.ejs | 40 ++++++++++++++++++++++++++++++-------- 7 files changed, 95 insertions(+), 43 deletions(-) diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index a1274ba..e85a1c1 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -83,6 +83,12 @@ button::-moz-focus-inner, input::-moz-focus-inner { label[for], button, select, summary, [type=radio], [type=submit], [type=checkbox] { cursor: pointer; } +svg.hidden { + position:absolute; + clip:rect(0 0 0 0); + border:0; + margin:-1px; +} /* touch devices, and anything other where there is no mouse */ @media screen and (pointer: coarse) { @@ -151,13 +157,36 @@ Neutral: #131211 overflow:hidden; padding:0; position:absolute; - width:1px + width:1px; } + .cut { --cut: 2em; clip-path: polygon(0% var(--cut), var(--cut) 0%, 100% 0, 100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%); } - + +.bg { + --bg1: color-mix(in oklch, var(--main) 95%, white); + --bg2: var(--main); + + [data-mui-color-scheme="dark"] & { + --bg1: color-mix(in oklch, var(--neutral) 90%, black); + --bg2: var(--neutral); + } +} +.bg-pattern { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + z-index: -1; +} + +.rough-edges { + filter: url(#rough-edges); +} + + @media screen and (prefers-reduced-motion: reduce), diff --git a/src/client/css/login.css b/src/client/css/login.css index a764b90..13a3704 100644 --- a/src/client/css/login.css +++ b/src/client/css/login.css @@ -5,13 +5,9 @@ } .login { - --bg1: color-mix(in oklch, var(--main) 95%, white); - --bg2: var(--main); --text: color-mix(in oklch, var(--neutral) 50%, black); [data-mui-color-scheme="dark"] & { - --bg1: color-mix(in oklch, var(--neutral) 90%, black); - --bg2: var(--neutral); --text: var(--main); } @@ -22,22 +18,31 @@ flex-wrap: wrap; color: var(--text); - background: repeating-linear-gradient(45deg, - var(--bg1), - var(--bg1) 5%, - var(--bg2) 5%, - var(--bg2) 10%); - background-size: 200px 200px; - animation: move-it 10s linear infinite; + .wrapper { max-width: 50em; padding: 2.5em; margin: 2.5em; - background: var(--semiBg); + position: relative; + filter: drop-shadow(10px 10px var(--semiBg)); + + &::after { + content: ""; + position: absolute; + inset: 0; + background: var(--semiBg); + filter: url(#rough-edges); + z-index: -1; + } + --cut: 3em; - .cut { --cut: 1.1em } /* reset for child elements */ + .cut { /* reset for child elements */ + --cut: 1.1em + } + + } .headline { @@ -45,7 +50,6 @@ padding-block: 1em; text-align: center; font-size: 3.5rem; - flex-basis: 100%; } form { @@ -113,7 +117,9 @@ font-style: italics; margin: 0; - strong { margin-right: 0.3em;} + strong { + margin-right: 0.3em; + } } diff --git a/src/client/css/start.css b/src/client/css/start.css index aec0da3..a5548f6 100644 --- a/src/client/css/start.css +++ b/src/client/css/start.css @@ -5,13 +5,9 @@ } .start { - --bg1: color-mix(in oklch, var(--main) 95%, white); - --bg2: var(--main); --text: color-mix(in oklch, var(--neutral) 50%, black); [data-mui-color-scheme="dark"] & { - --bg1: color-mix(in oklch, var(--neutral) 90%, black); - --bg2: var(--neutral); --text: var(--main); } @@ -22,15 +18,6 @@ flex-wrap: wrap; color: var(--text); - background: repeating-linear-gradient(45deg, - var(--bg1), - var(--bg1) 5%, - var(--bg2) 5%, - var(--bg2) 10%); - background-size: 200px 200px; - animation: move-it 10s linear infinite; - - .headline { margin-inline: auto; diff --git a/src/client/index.tsx b/src/client/index.tsx index 0cf03eb..1293afa 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Root, createRoot } from 'react-dom/client'; -import { Experimental_CssVarsProvider as CssVarsProvider, experimental_extendTheme as extendTheme} from '@mui/material/styles'; +import { Experimental_CssVarsProvider as CssVarsProvider, experimental_extendTheme as extendTheme } from '@mui/material/styles'; import App from "./components/App"; const theme = extendTheme({ // color pallette overwritten in css diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index df8eb4d..6e39809 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -170,9 +170,11 @@ function Login() { Login
-
+ + +
) } diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 752f6b4..377955e 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -3,14 +3,18 @@ import { Button, Typography } from '@mui/material'; import "../css/start.css"; function Start() { - return ( -
+ return ( +

Hello, React!!

Test Headline + + + +
- ) + ) } export default Start \ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs index dbacecc..fac0ffb 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -4,26 +4,26 @@ - + LOREX - Osmand Webtracking Frontend - + - + - + - + - + @@ -33,6 +33,30 @@

Lorex

- - + + + + \ No newline at end of file From 9718083673dd5e7343140f6dc33fc004978c352d Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 29 May 2024 01:59:11 +0200 Subject: [PATCH 130/206] [Task] #61, main headline style --- httpdocs/css/base.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index e85a1c1..b5a1b08 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -186,7 +186,12 @@ Neutral: #131211 filter: url(#rough-edges); } - +.headline { + font-variation-settings: "YOPQ" 32; + font-style: oblique 7.11deg; + font-stretch: 110%; + font-weight: 831; +} @media screen and (prefers-reduced-motion: reduce), From 9e1c22d5e882a2ee162f4776d0b204e0e72881ac Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 29 May 2024 15:49:32 +0200 Subject: [PATCH 131/206] [Task] #61, fine tune background pattern --- views/index.ejs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/views/index.ejs b/views/index.ejs index fac0ffb..d3ec370 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -37,9 +37,10 @@ From a2626eee83db4a5e5ed720a5fa63364dbd8256d5 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 29 May 2024 15:49:55 +0200 Subject: [PATCH 132/206] [Task] #61, font-weight reduced in darkmode --- httpdocs/css/base.css | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index b5a1b08..2114f90 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -131,6 +131,10 @@ Neutral: #131211 --textOnColor: var(--neutral); --semiBg: #ffffffbb; + --baseFontWeightModifier: 50; + + font-weight: calc(400 + var(--baseFontWeightModifier)); + /* dark theme, initial state (prefers mq) by react */ &[data-mui-color-scheme="dark"] { --main: oklch(75% 0.1738 64.55); @@ -141,6 +145,8 @@ Neutral: #131211 --bg: color-mix(in oklch, var(--neutral) 20%, black); --text: color-mix(in oklch, var(--neutral) 20%, white); --semiBg: #00000077; + + --baseFontWeightModifier: -50; } } @@ -166,11 +172,11 @@ Neutral: #131211 } .bg { - --bg1: color-mix(in oklch, var(--main) 95%, white); + --bg1: color-mix(in oklch, var(--main) 90%, white); --bg2: var(--main); [data-mui-color-scheme="dark"] & { - --bg1: color-mix(in oklch, var(--neutral) 90%, black); + --bg1: color-mix(in oklch, var(--neutral) 90%, white); --bg2: var(--neutral); } } @@ -190,7 +196,7 @@ Neutral: #131211 font-variation-settings: "YOPQ" 32; font-style: oblique 7.11deg; font-stretch: 110%; - font-weight: 831; + font-weight: calc(800 + var(--baseFontWeightModifier)); } @media screen and From 0638f105112c035fea097430da2230ea6d6f5e31 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 29 May 2024 18:18:18 +0200 Subject: [PATCH 133/206] [Task] #64, login design improvements --- src/client/css/login.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client/css/login.css b/src/client/css/login.css index 13a3704..5a02f5c 100644 --- a/src/client/css/login.css +++ b/src/client/css/login.css @@ -27,12 +27,17 @@ margin: 2.5em; position: relative; filter: drop-shadow(10px 10px var(--semiBg)); + [data-mui-color-scheme="dark"] & { + filter: none; + &::after { box-shadow: var(--text) 0 0 1em;} + } &::after { content: ""; position: absolute; inset: 0; background: var(--semiBg); + filter: url(#rough-edges); z-index: -1; } From d618d46b9942e8c6ebd6fb4b480e23f114cb69b7 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 3 Jun 2024 15:18:21 +0200 Subject: [PATCH 134/206] [Task] #61, update design with minor ripples and edges --- httpdocs/css/base.css | 4 ++-- views/index.ejs | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index 2114f90..d1cab3a 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -183,9 +183,9 @@ Neutral: #131211 .bg-pattern { position: absolute; inset: 0; - width: 100%; - height: 100%; + width: 100%; height: 100%; z-index: -1; + filter: url(#rough-light); } .rough-edges { diff --git a/views/index.ejs b/views/index.ejs index d3ec370..6ccb8bf 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -58,6 +58,14 @@ + + + + + + + + From 59ea9c725f367c654846081cb7142587d10de048 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 3 Jun 2024 15:19:00 +0200 Subject: [PATCH 135/206] [Task] #70, store token after login --- src/client/css/login.css | 13 ++++++++++--- src/client/pages/Login.tsx | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/client/css/login.css b/src/client/css/login.css index 5a02f5c..1973a3c 100644 --- a/src/client/css/login.css +++ b/src/client/css/login.css @@ -116,16 +116,23 @@ } } - .errorMessage { - color: var(--alert); + .message { font-size: 1.8em; font-style: italics; margin: 0; strong { - margin-right: 0.3em; + display: inline-block; + vertical-align: middle; + margin-right: 0.4em; } } + .message--error { + color: var(--alert); + } + .message--success { + color: var(--success); + } } \ No newline at end of file diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index 6e39809..56506ba 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { TextField, Button, InputAdornment, CircularProgress } from '@mui/material'; -import { AccountCircle, Lock, HighlightOff, Login as LoginIcon } from '@mui/icons-material'; +import { AccountCircle, Lock, HighlightOff, Login as LoginIcon, Check } from '@mui/icons-material'; import "../css/login.css"; import ModeSwitcher from '../components/ModeSwitcher'; import axios from 'axios'; @@ -21,7 +21,7 @@ function Login() { token: "" }); const [isLoading, setLoading] = React.useState(false); - const [errorObj, setErrorObj] = React.useState({ status: null, message: null }); + const [errorObj, setMessageObj] = React.useState({ isError: null, status: null, message: null }); const isFormValid = formInfo.user.value && !formInfo.user.isError && formInfo.password.value && !formInfo.password.isError; //&& formInfo.token; @@ -44,7 +44,7 @@ function Login() { async function submit(e) { e.preventDefault(); setLoading(true); - setErrorObj({ status: null, message: null }) + setMessageObj({ isError: null, status: null, message: null }) let token = null; // get csrf token @@ -59,7 +59,7 @@ function Login() { }) updateFormInfo({ ...formInfo, token: token.data }); } catch (error) { - setErrorObj({ status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) + setMessageObj({isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) console.log(error); } @@ -68,15 +68,17 @@ function Login() { // collect data and convert to urlencoded string then send const bodyFormData = { "user": formInfo.user.value, "password": formInfo.password.value, csrfToken: token.data }; try { - // nextup store token for further usuage - await axios({ + const response = await axios({ method: "post", url: "/login", data: qs.stringify(bodyFormData), headers: { "content-type": "application/x-www-form-urlencoded" } }) + const token = response.data.token; + sessionStorage.setItem("jwt", token); + setMessageObj({isError: false, status: , message: "Success!" }) } catch (error) { - setErrorObj({ status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) + setMessageObj({isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) console.log(error); } finally { setLoading(false); // Reset loading after request is complete @@ -149,7 +151,7 @@ function Login() {
{errorObj.status ? ( -

+

{errorObj.status} {errorObj.message.split('\n').map((line:string, index:string) => ( From 02018aba5aae32f055dc6660349e0b07b492cb72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:01:29 +0200 Subject: [PATCH 136/206] Bump braces from 3.0.2 to 3.0.3 (#76) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 08cc87d..d14403f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3462,12 +3462,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -5445,9 +5445,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" From 270b0b480a89bc193153df36d17606add6c77dad Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 26 Jun 2024 16:02:42 +0200 Subject: [PATCH 137/206] fix: upgrade @mui/icons-material from 5.15.16 to 5.15.18 (#75) Snyk has created this PR to upgrade @mui/icons-material from 5.15.16 to 5.15.18. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d14403f..0f44935 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.16", + "@mui/icons-material": "^5.15.18", "@mui/material": "^5.15.16", "axios": "^1.7.1", "bcrypt": "^5.1.1", @@ -1704,9 +1704,10 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.15.16", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.16.tgz", - "integrity": "sha512-s8vYbyACzTNZRKv+20fCfVXJwJqNcVotns2EKnu1wmAga6wv2LAo5kB1d5yqQqZlMFtp34EJvRXf7cy8X0tJVA==", + "version": "5.15.18", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.18.tgz", + "integrity": "sha512-jGhyw02TSLM0NgW+MDQRLLRUD/K4eN9rlK2pTBTL1OtzyZmQ8nB060zK1wA0b7cVrIiG+zyrRmNAvGWXwm2N9Q==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" }, diff --git a/package.json b/package.json index 3132a24..fa0e921 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.16", + "@mui/icons-material": "^5.15.18", "@mui/material": "^5.15.16", "axios": "^1.7.1", "bcrypt": "^5.1.1", From 809e809ce9d8458fa7aa9f588b192e13dc50eadd Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 26 Jun 2024 16:02:56 +0200 Subject: [PATCH 138/206] fix: upgrade react-router-dom from 6.23.0 to 6.23.1 (#74) Snyk has created this PR to upgrade react-router-dom from 6.23.0 to 6.23.1. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 29 ++++++++++++++++------------- package.json | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f44935..bbd48df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "module-alias": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.23.0", + "react-router-dom": "^6.23.1", "toobusy-js": "^0.5.1" }, "devDependencies": { @@ -1954,9 +1954,10 @@ } }, "node_modules/@remix-run/router": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.0.tgz", - "integrity": "sha512-Quz1KOffeEf/zwkCBM3kBtH4ZoZ+pT3xIXBG4PPW/XFtDP7EGhtTiC2+gpL9GnR7+Qdet5Oa6cYSvwKYg6kN9Q==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -11480,11 +11481,12 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-router": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.0.tgz", - "integrity": "sha512-wPMZ8S2TuPadH0sF5irFGjkNLIcRvOSaEe7v+JER8508dyJumm6XZB1u5kztlX0RVq6AzRVndzqcUh6sFIauzA==", + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.16.0" + "@remix-run/router": "1.16.1" }, "engines": { "node": ">=14.0.0" @@ -11494,12 +11496,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.0.tgz", - "integrity": "sha512-Q9YaSYvubwgbal2c9DJKfx6hTNoBp3iJDsl+Duva/DwxoJH+OTXkxGpql4iUK2sla/8z4RpjAm6EWx1qUDuopQ==", + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.16.0", - "react-router": "6.23.0" + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" }, "engines": { "node": ">=14.0.0" diff --git a/package.json b/package.json index fa0e921..32354b8 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "module-alias": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.23.0", + "react-router-dom": "^6.23.1", "toobusy-js": "^0.5.1" }, "_moduleAliases": { From 951780d597c65fa366ba37b3bc9e0a7caeeddd67 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 26 Jun 2024 16:03:17 +0200 Subject: [PATCH 139/206] fix: upgrade express-slow-down from 2.0.2 to 2.0.3 (#73) Snyk has created this PR to upgrade express-slow-down from 2.0.2 to 2.0.3. See this package in npm: express-slow-down See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index bbd48df..2accda0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "ejs": "^3.1.10", "express": "^4.19.2", "express-rate-limit": "^7.2.0", - "express-slow-down": "^2.0.2", + "express-slow-down": "^2.0.3", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", @@ -5320,9 +5320,9 @@ } }, "node_modules/express-slow-down": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/express-slow-down/-/express-slow-down-2.0.2.tgz", - "integrity": "sha512-Ql2gg6bBFVzwGUFLOPRcy8JkAMEinLZcdUpClIzsAizWh7AFvq93NHx7naugl73e/UoChncKuw5hQURBeSYtVg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/express-slow-down/-/express-slow-down-2.0.3.tgz", + "integrity": "sha512-vATCiFd8uQHtTeK5/Q0nLUukhZh+RV5zkcHxLQr0X5dEFVEYqzVXEe48nW23Z49fwtR+ApD9zn9sZRisTCR99w==", "license": "MIT", "dependencies": { "express-rate-limit": "7" @@ -5331,7 +5331,7 @@ "node": ">= 16" }, "peerDependencies": { - "express": "^4.0.0 || ^5.0.0" + "express": "4 || 5 || ^5.0.0-beta.1" } }, "node_modules/express-validator": { diff --git a/package.json b/package.json index 32354b8..4a2c0a2 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "ejs": "^3.1.10", "express": "^4.19.2", "express-rate-limit": "^7.2.0", - "express-slow-down": "^2.0.2", + "express-slow-down": "^2.0.3", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", From 6e520ee50c6670e3b1979ff2ed65dc5d4c51b17c Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 4 Jul 2024 15:48:04 +0200 Subject: [PATCH 140/206] fix: upgrade express-rate-limit from 7.2.0 to 7.3.0 (#82) Snyk has created this PR to upgrade express-rate-limit from 7.2.0 to 7.3.0. See this package in npm: express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2accda0..06663bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "dotenv": "^16.4.5", "ejs": "^3.1.10", "express": "^4.19.2", - "express-rate-limit": "^7.2.0", + "express-rate-limit": "^7.3.0", "express-slow-down": "^2.0.3", "express-validator": "^7.0.1", "helmet": "^7.1.0", @@ -5306,9 +5306,10 @@ } }, "node_modules/express-rate-limit": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.2.0.tgz", - "integrity": "sha512-T7nul1t4TNyfZMJ7pKRKkdeVJWa2CqB8NA1P8BwYaoDI5QSBZARv5oMS43J7b7I5P+4asjVXjb7ONuwDKucahg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.0.tgz", + "integrity": "sha512-ZPfWlcQQ1PsZonB/vqksOsBQV74z5osi/QcdoBCyKJXl/wOVjS1yRDmvkpMM52KJeLbiF2+djwVEnEgVCDdvtw==", + "license": "MIT", "engines": { "node": ">= 16" }, diff --git a/package.json b/package.json index 4a2c0a2..abaef64 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "dotenv": "^16.4.5", "ejs": "^3.1.10", "express": "^4.19.2", - "express-rate-limit": "^7.2.0", + "express-rate-limit": "^7.3.0", "express-slow-down": "^2.0.3", "express-validator": "^7.0.1", "helmet": "^7.1.0", From 33d6dbcfbacc314330daff7c347559d53fac314d Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 4 Jul 2024 15:48:18 +0200 Subject: [PATCH 141/206] fix: upgrade express-validator from 7.0.1 to 7.1.0 (#81) Snyk has created this PR to upgrade express-validator from 7.0.1 to 7.1.0. See this package in npm: express-validator See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 18 ++++++++++-------- package.json | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06663bf..b6b19a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "express": "^4.19.2", "express-rate-limit": "^7.3.0", "express-slow-down": "^2.0.3", - "express-validator": "^7.0.1", + "express-validator": "^7.1.0", "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", @@ -5336,12 +5336,13 @@ } }, "node_modules/express-validator": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", - "integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.1.0.tgz", + "integrity": "sha512-ePn6NXjHRZiZkwTiU1Rl2hy6aUqmi6Cb4/s8sfUsKH7j2yYl9azSpl8xEHcOj1grzzQ+UBEoLWtE1s6FDxW++g==", + "license": "MIT", "dependencies": { "lodash": "^4.17.21", - "validator": "^13.9.0" + "validator": "~13.12.0" }, "engines": { "node": ">= 8.0.0" @@ -12932,9 +12933,10 @@ } }, "node_modules/validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "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" } diff --git a/package.json b/package.json index abaef64..ec38b4e 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "express": "^4.19.2", "express-rate-limit": "^7.3.0", "express-slow-down": "^2.0.3", - "express-validator": "^7.0.1", + "express-validator": "^7.1.0", "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", From 740bacd7ab7fa0af1322060e778ce345f62b3c69 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 4 Jul 2024 15:48:37 +0200 Subject: [PATCH 142/206] fix: upgrade axios from 1.7.1 to 1.7.2 (#80) Snyk has created this PR to upgrade axios from 1.7.1 to 1.7.2. See this package in npm: axios See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6b19a8..5c41019 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.18", "@mui/material": "^5.15.16", - "axios": "^1.7.1", + "axios": "^1.7.2", "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", @@ -3281,9 +3281,10 @@ } }, "node_modules/axios": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.1.tgz", - "integrity": "sha512-+LV37nQcd1EpFalkXksWNBiA17NZ5m5/WspmHGmZmdx1qBOg/VNq/c4eRJiA9VQQHBOs+N0ZhhdU10h2TyNK7Q==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", diff --git a/package.json b/package.json index ec38b4e..0769c40 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.18", "@mui/material": "^5.15.16", - "axios": "^1.7.1", + "axios": "^1.7.2", "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", From ea7bf0478c92cad9ddfc07798e6c94f13ccdb8ad Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 4 Jul 2024 15:48:53 +0200 Subject: [PATCH 143/206] fix: upgrade @mui/icons-material from 5.15.18 to 5.15.19 (#79) Snyk has created this PR to upgrade @mui/icons-material from 5.15.18 to 5.15.19. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c41019..1efddd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.18", + "@mui/icons-material": "^5.15.19", "@mui/material": "^5.15.16", "axios": "^1.7.2", "bcrypt": "^5.1.1", @@ -1704,9 +1704,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.15.18", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.18.tgz", - "integrity": "sha512-jGhyw02TSLM0NgW+MDQRLLRUD/K4eN9rlK2pTBTL1OtzyZmQ8nB060zK1wA0b7cVrIiG+zyrRmNAvGWXwm2N9Q==", + "version": "5.15.19", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.19.tgz", + "integrity": "sha512-RsEiRxA5azN9b8gI7JRqekkgvxQUlitoBOtZglflb8cUDyP12/cP4gRwhb44Ea1/zwwGGjAj66ZJpGHhKfibNA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" diff --git a/package.json b/package.json index 0769c40..f9b3fc6 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.18", + "@mui/icons-material": "^5.15.19", "@mui/material": "^5.15.16", "axios": "^1.7.2", "bcrypt": "^5.1.1", From 3a1f9c894c8b55b7d0fc2f17d37e28dfa2d87cc6 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 16 Jul 2024 20:15:17 +0200 Subject: [PATCH 144/206] fix: upgrade @mui/icons-material from 5.15.19 to 5.15.20 (#88) Snyk has created this PR to upgrade @mui/icons-material from 5.15.19 to 5.15.20. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1efddd4..8d149e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.19", + "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.16", "axios": "^1.7.2", "bcrypt": "^5.1.1", @@ -1704,9 +1704,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.15.19", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.19.tgz", - "integrity": "sha512-RsEiRxA5azN9b8gI7JRqekkgvxQUlitoBOtZglflb8cUDyP12/cP4gRwhb44Ea1/zwwGGjAj66ZJpGHhKfibNA==", + "version": "5.15.20", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.20.tgz", + "integrity": "sha512-oGcKmCuHaYbAAoLN67WKSXtHmEgyWcJToT1uRtmPyxMj9N5uqwc/mRtEnst4Wj/eGr+zYH2FiZQ79v9k7kSk1Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" diff --git a/package.json b/package.json index f9b3fc6..094c30e 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.19", + "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.16", "axios": "^1.7.2", "bcrypt": "^5.1.1", From ef310add5b626dfb6a5edb4d97c5d0fe22b05ebf Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 16 Jul 2024 20:15:50 +0200 Subject: [PATCH 145/206] fix: upgrade react-router-dom from 6.23.1 to 6.24.0 (#91) Snyk has created this PR to upgrade react-router-dom from 6.23.1 to 6.24.0. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 26 +++++++++++++------------- package.json | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d149e6..155139c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "module-alias": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.23.1", + "react-router-dom": "^6.24.0", "toobusy-js": "^0.5.1" }, "devDependencies": { @@ -1954,9 +1954,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", - "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.0.tgz", + "integrity": "sha512-2D6XaHEVvkCn682XBnipbJjgZUU7xjLtA4dGJRBVUKpEaDYOZMENZoZjAOSb7qirxt5RupjzZxz4fK2FO+EFPw==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -11484,12 +11484,12 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-router": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", - "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.0.tgz", + "integrity": "sha512-sQrgJ5bXk7vbcC4BxQxeNa5UmboFm35we1AFK0VvQaz9g0LzxEIuLOhHIoZ8rnu9BO21ishGeL9no1WB76W/eg==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.16.1" + "@remix-run/router": "1.17.0" }, "engines": { "node": ">=14.0.0" @@ -11499,13 +11499,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", - "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.24.0.tgz", + "integrity": "sha512-960sKuau6/yEwS8e+NVEidYQb1hNjAYM327gjEyXlc6r3Skf2vtwuJ2l7lssdegD2YjoKG5l8MsVyeTDlVeY8g==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.16.1", - "react-router": "6.23.1" + "@remix-run/router": "1.17.0", + "react-router": "6.24.0" }, "engines": { "node": ">=14.0.0" diff --git a/package.json b/package.json index 094c30e..0e81109 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "module-alias": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.23.1", + "react-router-dom": "^6.24.0", "toobusy-js": "^0.5.1" }, "_moduleAliases": { From a4c195adfd3bd7bfae93d119935c5f6576e9f577 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 16 Jul 2024 20:34:36 +0200 Subject: [PATCH 146/206] 77 design base layout (#85) * [Task] #77 1st draft layout * [Change] #70 update token expire date * [Temp] #77, log out data on valid request, temp: error handling and display * [Temp] * [Task] #77, login Button functionality, default state * [Task] #77, removed outdated comments * [Task] #77, introduced linearBuffer Bar for login * [Task] #77, added modeSwticher to start page * [Task] #77, display last entry on map demo * [Task] #77, enhance login, show pastUser if availabe, show user on mainpage * [!!!Task] #77 first draft of functionality * [Task] #77 move map to new location * [Task] #77 create testData * [Fix] #77 codeFactor complains --- .vscode/settings.json | 4 +- httpdocs/css/base.css | 25 ++- jest.config.js | 2 +- jest.testData.config.js | 10 ++ package-lock.json | 107 +++++++++---- package.json | 5 + src/client/components/App.tsx | 31 +++- src/client/components/LinearBuffer.tsx | 46 ++++++ src/client/components/Map.tsx | 44 ++++++ src/client/components/ModeSwitcher.tsx | 2 +- src/client/components/Status.tsx | 16 ++ src/client/css/login.css | 37 +++-- src/client/css/map.module.css | 3 + src/client/css/modeSwticher.module.css | 5 +- src/client/css/start.css | 205 +++++++++++++++++++++++-- src/client/css/status.module.css | 0 src/client/pages/Login.tsx | 57 +++++-- src/client/pages/Start.tsx | 151 +++++++++++++++++- src/client/tsconfig.json | 2 +- src/scripts/token.ts | 8 +- src/testData/createTestData.test.ts | 68 ++++++++ types.d.ts | 2 +- 22 files changed, 722 insertions(+), 108 deletions(-) create mode 100644 jest.testData.config.js create mode 100644 src/client/components/LinearBuffer.tsx create mode 100644 src/client/components/Map.tsx create mode 100644 src/client/components/Status.tsx create mode 100644 src/client/css/map.module.css create mode 100644 src/client/css/status.module.css create mode 100644 src/testData/createTestData.test.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 3311857..8d2c763 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,7 @@ "workbench.editor.enablePreview": false, "editor.rename.enablePreview": false, "typescript.tsdk": "node_modules\\typescript\\lib", - "html.format.wrapLineLength": 150 + "html.format.wrapLineLength": 150, + "javascript.preferences.quoteStyle": "double", + "typescript.preferences.quoteStyle": "double" } \ No newline at end of file diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index d1cab3a..fd4fe7f 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -135,6 +135,8 @@ Neutral: #131211 font-weight: calc(400 + var(--baseFontWeightModifier)); + accent-color: var(--main); + /* dark theme, initial state (prefers mq) by react */ &[data-mui-color-scheme="dark"] { --main: oklch(75% 0.1738 64.55); @@ -166,7 +168,7 @@ Neutral: #131211 width:1px; } -.cut { +.cut, .cut-after::after { --cut: 2em; clip-path: polygon(0% var(--cut), var(--cut) 0%, 100% 0, 100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%); } @@ -199,6 +201,15 @@ Neutral: #131211 font-weight: calc(800 + var(--baseFontWeightModifier)); } +.fade { animation: fade 1s 1s forwards; } +.fadeIn { animation: reverse fade 1s forwards; } +@keyframes fade { + to { + font-size: 0; + opacity: 0; + } +} + @media screen and (prefers-reduced-motion: reduce), (update: slow) { @@ -377,12 +388,12 @@ Neutral: #131211 /* --mui-palette-FilledInput-bg:rgba(0, 0, 0, 0.06); --mui-palette-FilledInput-hoverBg:rgba(0, 0, 0, 0.09); --mui-palette-FilledInput-disabledBg:rgba(0, 0, 0, 0.12); */ - --mui-palette-LinearProgress-primaryBg: color-mix(in oklch, var(--mui-palette-primary-main) 85%, transparent); - --mui-palette-LinearProgress-secondaryBg: color-mix(in oklch, var(--mui-palette-secondary-main) 85%, transparent); - --mui-palette-LinearProgress-errorBg: color-mix(in oklch, var(--mui-error-primary-main) 85%, transparent); - --mui-palette-LinearProgress-infoBg: color-mix(in oklch, var(--mui-palette-info-main) 85%, transparent); - --mui-palette-LinearProgress-successBg: color-mix(in oklch, var(--mui-palette-success-main) 85%, transparent); - --mui-palette-LinearProgress-warningBg: color-mix(in oklch, var(--mui-palette-warning-main) 85%, transparent); + --mui-palette-LinearProgress-primaryBg: color-mix(in oklch, var(--mui-palette-primary-main) 50%, transparent); + --mui-palette-LinearProgress-secondaryBg: color-mix(in oklch, var(--mui-palette-secondary-main) 50%, transparent); + --mui-palette-LinearProgress-errorBg: color-mix(in oklch, var(--mui-error-primary-main) 50%, transparent); + --mui-palette-LinearProgress-infoBg: color-mix(in oklch, var(--mui-palette-info-main) 50%, transparent); + --mui-palette-LinearProgress-successBg: color-mix(in oklch, var(--mui-palette-success-main) 50%, transparent); + --mui-palette-LinearProgress-warningBg: color-mix(in oklch, var(--mui-palette-warning-main) 50%, transparent); --mui-palette-Skeleton-bg: rgba(var(--mui-palette-text-primaryChannel) / 0.11); --mui-palette-Slider-primaryTrack: var(--mui-palette-primary-main); --mui-palette-Slider-secondaryTrack: var(--mui-palette-secondary-main); diff --git a/jest.config.js b/jest.config.js index 0a57f54..5cada10 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,7 +2,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - modulePathIgnorePatterns: ['/dist/'], + modulePathIgnorePatterns: ['/dist/', '/src/testData/'], moduleNameMapper: { '^@src/(.*)$': '/src/$1', }, diff --git a/jest.testData.config.js b/jest.testData.config.js new file mode 100644 index 0000000..f8e72a3 --- /dev/null +++ b/jest.testData.config.js @@ -0,0 +1,10 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + moduleNameMapper: { + '^@src/(.*)$': '/src/$1', + }, + testMatch: ['/src/testData/createTestData.test.ts'], + bail: true +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 155139c..6c6a756 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,9 +25,12 @@ "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", + "leaflet": "^1.9.4", + "leaflet-defaulticon-compatibility": "^0.1.2", "module-alias": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-leaflet": "^4.2.1", "react-router-dom": "^6.24.0", "toobusy-js": "^0.5.1" }, @@ -40,6 +43,7 @@ "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.6", + "@types/leaflet": "^1.9.12", "@types/node": "^20.11.30", "@types/react": "^18.2.74", "@types/react-dom": "^18.2.24", @@ -1704,9 +1708,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.15.20", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.20.tgz", - "integrity": "sha512-oGcKmCuHaYbAAoLN67WKSXtHmEgyWcJToT1uRtmPyxMj9N5uqwc/mRtEnst4Wj/eGr+zYH2FiZQ79v9k7kSk1Q==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.4.tgz", + "integrity": "sha512-j9/CWctv6TH6Dou2uR2EH7UOgu79CW/YcozxCYVLJ7l03pCsiOlJ5sBArnWJxJ+nGkFwyL/1d1k8JEPMDR125A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" @@ -1953,10 +1957,20 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@remix-run/router": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.0.tgz", - "integrity": "sha512-2D6XaHEVvkCn682XBnipbJjgZUU7xjLtA4dGJRBVUKpEaDYOZMENZoZjAOSb7qirxt5RupjzZxz4fK2FO+EFPw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", + "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -2144,6 +2158,13 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2223,6 +2244,16 @@ "@types/node": "*" } }, + "node_modules/@types/leaflet": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.12.tgz", + "integrity": "sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -5307,9 +5338,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.0.tgz", - "integrity": "sha512-ZPfWlcQQ1PsZonB/vqksOsBQV74z5osi/QcdoBCyKJXl/wOVjS1yRDmvkpMM52KJeLbiF2+djwVEnEgVCDdvtw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.1.tgz", + "integrity": "sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==", "license": "MIT", "engines": { "node": ">= 16" @@ -5612,20 +5643,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7482,6 +7499,17 @@ "node": ">=0.10" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, + "node_modules/leaflet-defaulticon-compatibility": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/leaflet-defaulticon-compatibility/-/leaflet-defaulticon-compatibility-0.1.2.tgz", + "integrity": "sha512-IrKagWxkTwzxUkFIumy/Zmo3ksjuAu3zEadtOuJcKzuXaD76Gwvg2Z1mLyx7y52ykOzM8rAH5ChBs4DnfdGa6Q==", + "license": "BSD-2-Clause" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -11457,7 +11485,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -11469,7 +11496,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11483,13 +11509,26 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-router": { - "version": "6.24.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.0.tgz", - "integrity": "sha512-sQrgJ5bXk7vbcC4BxQxeNa5UmboFm35we1AFK0VvQaz9g0LzxEIuLOhHIoZ8rnu9BO21ishGeL9no1WB76W/eg==", + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.0.tgz", + "integrity": "sha512-bziKjCcDbcxgWS9WlWFcQIVZ2vJHnCP6DGpQDT0l+0PFDasfJKgzf9CM22eTyhFsZkjk8ApCdKjJwKtzqH80jQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.17.0" + "@remix-run/router": "1.18.0" }, "engines": { "node": ">=14.0.0" @@ -11499,13 +11538,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.24.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.24.0.tgz", - "integrity": "sha512-960sKuau6/yEwS8e+NVEidYQb1hNjAYM327gjEyXlc6r3Skf2vtwuJ2l7lssdegD2YjoKG5l8MsVyeTDlVeY8g==", + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.0.tgz", + "integrity": "sha512-BhcczgDWWgvGZxjDDGuGHrA8HrsSudilqTaRSBYLWDayvo1ClchNIDVt5rldqp6e7Dro5dEFx9Mzc+r292lN0w==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.17.0", - "react-router": "6.24.0" + "@remix-run/router": "1.18.0", + "react-router": "6.25.0" }, "engines": { "node": ">=14.0.0" diff --git a/package.json b/package.json index 0e81109..c0f5442 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "lint:client": "eslint httpdocs/js/ --fix", "lint:react": "eslint src/client/ --fix", "test": "jest", + "test:data": "jest --config jest.testData.config.js", "test:app": "jest src/tests/app.test.ts", "test:login": "jest src/tests/login.test.ts", "test:unit": "jest src/tests/unit.test.ts", @@ -33,6 +34,7 @@ "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.6", + "@types/leaflet": "^1.9.12", "@types/node": "^20.11.30", "@types/react": "^18.2.74", "@types/react-dom": "^18.2.24", @@ -77,9 +79,12 @@ "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", + "leaflet": "^1.9.4", + "leaflet-defaulticon-compatibility": "^0.1.2", "module-alias": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-leaflet": "^4.2.1", "react-router-dom": "^6.24.0", "toobusy-js": "^0.5.1" }, diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 8f879d6..0a05fac 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,8 +1,30 @@ -import React from 'react'; +import React, { createContext, useState } from 'react'; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import Start from '../pages/Start'; import Login from '../pages/Login'; +export const LoginContext = createContext([]); + +export function convertJwt() { + const token = localStorage?.jwt; + if (!token) { return false } + try { + const { user, exp } = JSON.parse(window.atob(token.split('.')[1])); + return { user, exp }; + } catch (error) { + console.error("Unable to parse JWT Data"); + return false; + } +} + +function loginDefault(userInfo) { + if (!userInfo) { return false; } + + const date = new Date(); + const exp = userInfo.exp + return date.getTime() / 1000 <= exp; +} + const router = createBrowserRouter([ { path: "/", @@ -15,10 +37,13 @@ const router = createBrowserRouter([ ]); const App = () => { - + const [userInfo, setUserInfo] = useState(convertJwt()); + const [isLoggedIn, setLogin] = useState(loginDefault(userInfo)); return ( - + + + ); } diff --git a/src/client/components/LinearBuffer.tsx b/src/client/components/LinearBuffer.tsx new file mode 100644 index 0000000..3b6bc86 --- /dev/null +++ b/src/client/components/LinearBuffer.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import LinearProgress from '@mui/material/LinearProgress'; + +export default function LinearBuffer({ msStart, msFinish, variant = "buffer" }: { msStart: number, msFinish: number, variant?: "buffer" | "determinate" }) { + const [progress, setProgress] = React.useState(0); + const [buffer, setBuffer] = React.useState(10); + + const progressRef = React.useRef(() => { }); + React.useEffect(() => { + if (!msStart || !msFinish) { + console.log("LinearProgress did not recieve correct data") + } + progressRef.current = () => { + let progressValue; + const duration = msFinish - msStart; // duration based on input props + const date = new Date(); + const now = date.getTime(); + const progressCalcValue = ((now - msStart) / duration) * 100; + progressValue = progressCalcValue; + if (variant == "buffer") { + const secondPhase = duration == 1000; + const bufferValue = secondPhase ? 100 : 90; + progressValue = secondPhase ? 100 : Math.min(progressCalcValue, bufferValue); + + setBuffer(bufferValue); + } + + setProgress(progressValue); + + }; + }); + + React.useEffect(() => { + const timer = setInterval(() => { + progressRef.current(); + }, 300); + + return () => { + clearInterval(timer); + }; + }, []); + + return ( + + ); +} \ No newline at end of file diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx new file mode 100644 index 0000000..3eb782a --- /dev/null +++ b/src/client/components/Map.tsx @@ -0,0 +1,44 @@ +import React, { useEffect } from 'react' +import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet' +import 'leaflet/dist/leaflet.css'; +import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css'; // Re-uses images from ~leaflet package +// import L from 'leaflet'; +import 'leaflet-defaulticon-compatibility'; +import * as css from "../css/map.module.css"; + + +// Used to recenter the map to new coordinates +const MapRecenter = ({ lat, lon, zoom }: { lat: number, lon: number, zoom: number }) => { + const map = useMap(); + + useEffect(() => { + // Fly to that coordinates and set new zoom level + map.flyTo([lat, lon], zoom); + }, [lat, lon]); + return null; + +}; + +function Map({ entries }: { entries: Models.IEntry[] }) { + if (!entries?.length) { + return No Data to be displayed + } + const lastEntry = entries.at(-1); + + return ( + + + + + + {JSON.stringify(lastEntry, null, 2)} + + + + ) +} + +export default Map diff --git a/src/client/components/ModeSwitcher.tsx b/src/client/components/ModeSwitcher.tsx index a9ca0d7..cf4bb31 100644 --- a/src/client/components/ModeSwitcher.tsx +++ b/src/client/components/ModeSwitcher.tsx @@ -16,7 +16,7 @@ function ModeSwitcher() {

+ {isLoading && }
diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 377955e..451508c 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -1,19 +1,154 @@ -import React from 'react' -import { Button, Typography } from '@mui/material'; +import React, { useEffect, useState, useContext, useRef } from 'react' import "../css/start.css"; +import axios from 'axios'; +import { LoginContext } from "../components/App"; +import { HighlightOff, Check } from '@mui/icons-material'; +import { Button } from '@mui/material'; +import ModeSwitcher from '../components/ModeSwitcher'; +import Map from '../components/Map'; +import Status from '../components/Status'; +import LinearBuffer from "../components/LinearBuffer"; + +function timeAgo(timestamp: number): string { + if (!Number.isInteger(timestamp)) { + return ""; + } + const now = Date.now(); + const diffInSeconds = Math.floor((now - timestamp) / 1000); + + const seconds = diffInSeconds; + const minutes = Math.floor(diffInSeconds / 60); + const hours = Math.floor(diffInSeconds / 3600); + const days = Math.floor(diffInSeconds / 86400); + const weeks = Math.floor(diffInSeconds / 604800); + const months = Math.floor(diffInSeconds / 2592000); + const years = Math.floor(diffInSeconds / 31536000); + + if (seconds < 8) { return "Instant"; } + else if (seconds < 30) { return "Just now" } + else if (seconds < 60) { return "a moment ago" } + else if (minutes < 60) { return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`; } + else if (hours < 24) { return `${hours} ${hours === 1 ? "hour" : "hours"} ago`; } + else if (days < 7) { return `${days} ${days === 1 ? "day" : "days"} ago`; } + else if (weeks < 4) { return `${weeks} ${weeks === 1 ? "week" : "weeks"} ago`; } + else if (months < 12) { return `${months} ${months === 1 ? "month" : "months"} ago`; } + else { return `${years} ${years === 1 ? "year" : "years"} ago`; } +} function Start() { + const [isLoggedIn, setLogin, userInfo] = useContext(LoginContext); + const [entries, setEntries] = useState([]); + const [messageObj, setMessageObj] = useState({ isError: null, status: null, message: null }); + const [lastFetch, setLastFetch] = useState(); + const [nextFetch, setNextFetch] = useState(); + + const index = useRef(0); + const intervalID = useRef(); + + const fetchIntervalMs = 1000 * 55; + + const getData = async () => { + const token = localStorage.getItem("jwt"); + let response; + + if (!token) { + setLogin(false); + setMessageObj({ isError: true, status: "403", message: "No valid login" }) + return false; + } + + try { + const now = new Date().getTime(); + setLastFetch(now); + response = await axios({ + method: 'get', + url: "/read?index=" + index.current + "&noCache=" + now, + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + const newEntries = response.data.entries; + if (newEntries.length) { + setEntries((prevEntries) => [prevEntries, ...newEntries]); + index.current += newEntries.length; + } + + setMessageObj({ isError: null, status: null, message: null }); + setNextFetch(new Date().getTime() + fetchIntervalMs); + } catch (error) { + clearInterval(intervalID.current); intervalID.current = null; + console.info("cleared Interval"); + setNextFetch(null); + if (error.response.status == 403) { setLogin(false) } + setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }); + } + }; + + useEffect(() => { + if (isLoggedIn) { + getData(); + intervalID.current = setInterval(getData, fetchIntervalMs); // capture interval ID as return from setInterval and pass to state + return () => { console.log("cleanup"); clearInterval(intervalID.current); intervalID.current = null; }; + } else if (userInfo) { // no valid login but userInfo + setMessageObj({ isError: true, status: "403", message: "Login expired" }) + } + }, []); + return ( -
-

Hello, React!!

- Test Headline - - + <> +
+
+ {messageObj.isError && +
+ {messageObj.status} {messageObj.message} +
+ } + {!messageObj.isError && userInfo && +
+ {userInfo.user} Welcome back +
+ } + +
+ +
+
+
+
+
image1
+
image2
+
image3
+
+
+ {isLoggedIn && intervalID && + + } + {isLoggedIn && intervalID && entries?.length > 0 && + <> + GPS: + {entries.at(-1).lat} / {entries.at(-1).lon} + {timeAgo(entries.at(-1).time.created)} + + } +
+
-
+ ) } diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index 04b7ddc..7777c58 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -6,5 +6,5 @@ "jsx": "react", "esModuleInterop": true }, - "include": ["**/*.tsx", "**/*.ts", "types.d.ts"] + "include": ["**/*.tsx", "**/*.ts", "types.d.ts", "../../types.d.ts"] } \ No newline at end of file diff --git a/src/scripts/token.ts b/src/scripts/token.ts index 78fb959..9a012f2 100644 --- a/src/scripts/token.ts +++ b/src/scripts/token.ts @@ -59,7 +59,11 @@ export function validateJWT(req: Request) { } catch (err) { let message = "could not verify"; if (err instanceof Error) { - message = `${err.name} - ${err.message}`; + if (err.name == "TokenExpiredError") { + message = "Login expired"; + } else { + message = `${err.name} - ${err.message}`; + } } return { success: false, status: 403, message: message }; @@ -82,7 +86,7 @@ export function createJWT(req: Request, res: Response) { date: dateString, user: req.body.user }; - const token = jwt.sign(payload, key, { expiresIn: 60 * 2 }); + const token = jwt.sign(payload, key, { expiresIn: 60 * 30 }); res.locals.token = token; return token; } diff --git a/src/testData/createTestData.test.ts b/src/testData/createTestData.test.ts new file mode 100644 index 0000000..9d1c789 --- /dev/null +++ b/src/testData/createTestData.test.ts @@ -0,0 +1,68 @@ +import axios, { AxiosError } from 'axios'; + + +async function callServer(timestamp = new Date().getTime(), query: string, expectStatus: number = 200, method: string = "HEAD") { + const url = new URL("http://localhost:80/write?"); + url.search = "?" + query; + const params = new URLSearchParams(url.search); + params.set("timestamp", timestamp.toString()); + url.search = params.toString(); + + + let response; + if (expectStatus == 200) { + if (method == "GET") { + response = await axios.get(url.toString()); + } else { + response = await axios.head(url.toString()); + } + expect(response.status).toBe(expectStatus); + } else { + try { + await axios.head(url.toString()); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + expect(axiosError.response.status).toBe(expectStatus); + } else { + console.error(axiosError); + } + } + } +} + + + + +describe('test Data', () => { + const entries = 5; + const start = { lat: 52.51625, lon: 13.37661 }; + const end = { lat: 52.50960, lon: 13.27457 }; + const diff = {lat: end.lat - start.lat, lon: end.lon - start.lon}; + // eslint-disable-next-line jest/expect-expect + it('create ' + entries + 'entries', () => { + return new Promise(done => { + + for (let i = 0; i <= entries; i++) { + const lat = start.lat + (diff.lat / entries * i); + const lon = start.lon + (diff.lon / entries * i); + setTimeout(async () => { + console.log("call server " + i); + await callServer(undefined, `user=xx&lat=${lat}&lon=${lon}×tamp=R3Pl4C3&hdop=${Math.floor(Math.random() * 15) + 1}&altitude=${i+1}&speed=${46 + i}&heading=${262 + Math.floor(Math.random() * 20) - 10}&key=test`, 200, "GET"); + }, 1000 * 30 * i); + } + + setTimeout(async () => { + done(); + }, 1000 * 30 * entries); + }) + }, 1000 * 30 * (entries + 1)); + +}); + + + + + + + diff --git a/types.d.ts b/types.d.ts index 3cde1ce..0bab5de 100644 --- a/types.d.ts +++ b/types.d.ts @@ -30,7 +30,7 @@ namespace File { namespace Models { interface IEntries { - entries: Models.IEntry[] + entries: IEntry[] } interface IEntry { From 431fc7b9283515e8f94c9226e17a442ec888180d Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 17 Jul 2024 18:16:25 +0200 Subject: [PATCH 147/206] [Task] #77, draft of status content --- src/client/components/Status.tsx | 51 +++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/client/components/Status.tsx b/src/client/components/Status.tsx index 2fa56c3..48a04bc 100644 --- a/src/client/components/Status.tsx +++ b/src/client/components/Status.tsx @@ -1,15 +1,58 @@ import React from 'react' //import * as css from "../css/status.module.css"; +function getStatusData(entries) { + const cleanEntries = entries.filter((entry: Models.IEntry) => !entry.ignore); + + function getMean(prop) { + const props = prop.split('.'); + let divider = 0; // cannot be hardcoded to cleanEntries.length because some properties don't exist on first or last dataPoint + const value = cleanEntries.reduce((accumulatorValue, current) => { + // find potentially nested value + let value = current; // overwritten recursively, start as current + for (const prop of props) { // iterate over split props (passed as parameter) + value = value[prop]; // replace current with the next level or finished value + } + + if (typeof value == "undefined") { + return accumulatorValue; + } + + divider++; // keep track of how many entries there are + return parseFloat(accumulatorValue) + parseFloat(value); + + }, 0) / divider; // now that all values have been added together, devide by amount of them + + //console.log(prop + ": " + value + " divider: " + divider); + return value; + } + + const ignoredEntries = entries.length - cleanEntries.length; + const uploadMean = getMean("time.uploadDuration").toFixed(3); + const speedGPSMean = (getMean("speed.gps") * 3.6).toFixed(1); + const speedCalcMean = (getMean("speed.horizontal") * 3.6).toFixed(1); + + return { + ignoredEntries, + uploadMean, + speedGPSMean, + speedCalcMean + } +} function Map({ entries }: { entries: Models.IEntry[] }) { - if(!entries?.length) { + if (!entries?.length) { return No Data to be displayed } - //const lastEntry = entries.at(-1); - + const statusData = getStatusData(entries); + const lastEntry = entries.at(-1); return ( - Status! +
    +
  • datapoints: {entries.length - statusData.ignoredEntries}({statusData.ignoredEntries})
  • +
  • Ø upload: {statusData.uploadMean}s
  • +
  • Ø speed: GPS: {statusData.speedGPSMean}km/h Calc: {statusData.speedCalcMean == "NaN" ? " - " : statusData.speedCalcMean }km/h
  • +
  • +
) } From d3667179f9af0fca5b052db2690f17906f605ff8 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 17 Jul 2024 18:17:02 +0200 Subject: [PATCH 148/206] [FIX] #77 change data accumulation --- src/client/pages/Start.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 451508c..a84ef6e 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -70,7 +70,7 @@ function Start() { const newEntries = response.data.entries; if (newEntries.length) { - setEntries((prevEntries) => [prevEntries, ...newEntries]); + setEntries((prevEntries) => [...prevEntries, ...newEntries]); index.current += newEntries.length; } @@ -97,6 +97,7 @@ function Start() { return ( <> + {console.info("entries %o", entries)}
{messageObj.isError && From 14551991fb67c520f3a1bcc45bff9383ec8343ec Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 17 Jul 2024 18:17:17 +0200 Subject: [PATCH 149/206] [Task] #77 improve test example data --- src/testData/createTestData.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/testData/createTestData.test.ts b/src/testData/createTestData.test.ts index 9d1c789..39b0c6c 100644 --- a/src/testData/createTestData.test.ts +++ b/src/testData/createTestData.test.ts @@ -35,20 +35,22 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec describe('test Data', () => { - const entries = 5; + const entries = 6; const start = { lat: 52.51625, lon: 13.37661 }; const end = { lat: 52.50960, lon: 13.27457 }; const diff = {lat: end.lat - start.lat, lon: end.lon - start.lon}; + // eslint-disable-next-line jest/expect-expect - it('create ' + entries + 'entries', () => { + it('create ' + entries + ' entries', () => { return new Promise(done => { - for (let i = 0; i <= entries; i++) { - const lat = start.lat + (diff.lat / entries * i); - const lon = start.lon + (diff.lon / entries * i); + for (let i = 0; i < entries; i++) { + const lat = (start.lat + (diff.lat / (entries - 1) * i)).toFixed(8); + const lon = (start.lon + (diff.lon / (entries - 1) * i)).toFixed(8); setTimeout(async () => { - console.log("call server " + i); - await callServer(undefined, `user=xx&lat=${lat}&lon=${lon}×tamp=R3Pl4C3&hdop=${Math.floor(Math.random() * 15) + 1}&altitude=${i+1}&speed=${46 + i}&heading=${262 + Math.floor(Math.random() * 20) - 10}&key=test`, 200, "GET"); + await callServer(undefined, `user=xx&lat=${lat}&lon=${lon}×tamp=R3Pl4C3&hdop=${Math.floor(Math.random() * 15) + 1}&altitude=${i+1}&speed=${35.5 + i}&heading=${262 + Math.floor(Math.random() * 20) - 10}&key=test`, 200, "GET"); + console.log("called server " + (i + 1) + "/" + entries); + }, 1000 * 30 * i); } From d516382b46295c31269539d008ff92128fdfef4c Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Jul 2024 14:35:47 +0200 Subject: [PATCH 150/206] fix: upgrade @mui/material from 5.15.16 to 5.15.20 (#92) Snyk has created this PR to upgrade @mui/material from 5.15.16 to 5.15.20. See this package in npm: @mui/material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 79 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c6a756..054cfd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.20", - "@mui/material": "^5.15.16", + "@mui/material": "^5.15.20", "axios": "^1.7.2", "bcrypt": "^5.1.1", "chalk": "^4.1.2", @@ -1699,9 +1699,10 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.15.16", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.16.tgz", - "integrity": "sha512-PTIbMJs5C/vYMfyJNW8ArOezh4eyHkg2pTeA7bBxh2kLP1Uzs0Nm+krXWbWGJPwTWjM8EhnDrr4aCF26+2oleg==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.4.tgz", + "integrity": "sha512-rNdHXhclwjEZnK+//3SR43YRx0VtjdHnUFhMSGYmAMJve+KiwEja/41EYh8V3pZKqF2geKyfcFUenTfDTYUR4w==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" @@ -1734,16 +1735,17 @@ } }, "node_modules/@mui/material": { - "version": "5.15.16", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.16.tgz", - "integrity": "sha512-ery2hFReewko9gpDBqOr2VmXwQG9ifXofPhGzIx09/b9JqCQC/06kZXZDGGrOTpIddK9HlIf4yrS+G70jPAzUQ==", + "version": "5.15.20", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.20.tgz", + "integrity": "sha512-tVq3l4qoXx/NxUgIx/x3lZiPn/5xDbdTE8VrLczNpfblLYZzlrbxA7kb9mI8NoBF6+w9WE9IrxWnKK5KlPI2bg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", "@mui/base": "5.0.0-beta.40", - "@mui/core-downloads-tracker": "^5.15.16", - "@mui/system": "^5.15.15", + "@mui/core-downloads-tracker": "^5.15.20", + "@mui/system": "^5.15.20", "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", + "@mui/utils": "^5.15.20", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", "csstype": "^3.1.3", @@ -1778,12 +1780,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", - "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.4.tgz", + "integrity": "sha512-ZsAm8cq31SJ37SVWLRlu02v9SRthxnfQofaiv14L5Bht51B0dz6yQEoVU/V8UduZDCCIrWkBHuReVfKhE/UuXA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.15.14", + "@mui/utils": "^5.16.4", "prop-types": "^15.8.1" }, "engines": { @@ -1804,9 +1807,10 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", - "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.4.tgz", + "integrity": "sha512-0+mnkf+UiAmTVB8PZFqOhqf729Yh0Cxq29/5cA3VAyDVTRIUUQ8FXQhiAhUIbijFmM72rY80ahFPXIm4WDbzcA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.11.0", @@ -1835,15 +1839,16 @@ } }, "node_modules/@mui/system": { - "version": "5.15.15", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz", - "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.4.tgz", + "integrity": "sha512-ET1Ujl2/8hbsD611/mqUuNArMCGv/fIWO/f8B3ZqF5iyPHM2aS74vhTNyjytncc4i6dYwGxNk+tLa7GwjNS0/w==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.15.14", - "@mui/styled-engine": "^5.15.14", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", + "@mui/private-theming": "^5.16.4", + "@mui/styled-engine": "^5.16.4", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.4", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1874,9 +1879,10 @@ } }, "node_modules/@mui/types": { - "version": "7.2.14", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", - "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", + "version": "7.2.15", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.15.tgz", + "integrity": "sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q==", + "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0" }, @@ -1887,14 +1893,16 @@ } }, "node_modules/@mui/utils": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", - "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.4.tgz", + "integrity": "sha512-nlppYwq10TBIFqp7qxY0SvbACOXeOjeVL3pOcDsK0FT8XjrEXh9/+lkg8AEIzD16z7YfiJDQjaJG2OLkE7BxNg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@types/prop-types": "^15.7.11", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "react-is": "^18.3.1" }, "engines": { "node": ">=12.0.0" @@ -11505,9 +11513,10 @@ } }, "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" }, "node_modules/react-leaflet": { "version": "4.2.1", diff --git a/package.json b/package.json index c0f5442..a3849f3 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.20", - "@mui/material": "^5.15.16", + "@mui/material": "^5.15.20", "axios": "^1.7.2", "bcrypt": "^5.1.1", "chalk": "^4.1.2", From b70c248cadc62e29f7b95463754c3dfc7dae01d9 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Jul 2024 15:06:02 +0200 Subject: [PATCH 151/206] [Fix] #94, refactor-ignore logic (multiline) (#95) Serverside: When writing entry, the most recent previous entry is checked wether to be ignored. Also if more than 2 items already exist meaning writing is preparing at least the 3rd entry, we recalculate distances and timing if previousItems are ignored. Frontend: In order to benefit and get the recent information that a previous item is being ignored, frontEnd askes for the current item again and merges it and following items. Remember the most recent item can never be ignored due to policy. Maybe there is no further writing, so I want to have the latest datapoint. --- src/client/components/Map.tsx | 15 ++++++++++----- src/client/components/Status.tsx | 2 +- src/client/pages/Start.tsx | 24 +++++++++++++++++++----- src/models/entry.ts | 29 +++++++++++++++++++++++------ src/scripts/ignore.ts | 4 ++-- src/tests/integration.test.ts | 19 +++++++++++++------ views/index.ejs | 6 +++--- 7 files changed, 71 insertions(+), 28 deletions(-) diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index 3eb782a..abfe558 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -23,7 +23,9 @@ function Map({ entries }: { entries: Models.IEntry[] }) { if (!entries?.length) { return No Data to be displayed } + const lastEntry = entries.at(-1); + const cleanEntries = entries.filter((entry) => !entry.ignore); return ( @@ -32,11 +34,14 @@ function Map({ entries }: { entries: Models.IEntry[] }) { attribution='© OpenStreetMap contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> - - - {JSON.stringify(lastEntry, null, 2)} - - + {cleanEntries.map((entry) => + + +
{JSON.stringify(entry, null, 2)}
+
+
+ )} +
) } diff --git a/src/client/components/Status.tsx b/src/client/components/Status.tsx index 48a04bc..5c1991e 100644 --- a/src/client/components/Status.tsx +++ b/src/client/components/Status.tsx @@ -44,7 +44,7 @@ function Map({ entries }: { entries: Models.IEntry[] }) { return No Data to be displayed } const statusData = getStatusData(entries); - const lastEntry = entries.at(-1); + //const lastEntry = entries.at(-1); return (
    diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index a84ef6e..25cf726 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -62,18 +62,32 @@ function Start() { setLastFetch(now); response = await axios({ method: 'get', - url: "/read?index=" + index.current + "&noCache=" + now, + url: "/read?index=" + (Math.max(index.current - 1, 0)) + "&noCache=" + now, headers: { 'Authorization': `Bearer ${token}` } }); const newEntries = response.data.entries; - if (newEntries.length) { - setEntries((prevEntries) => [...prevEntries, ...newEntries]); - index.current += newEntries.length; + + if (newEntries.length == 1) { + setEntries(newEntries); } + if (newEntries.length > 1) { + setEntries((prevEntries) => { + const allButLastPrevEntries = prevEntries.slice(0, prevEntries.length -1); + console.log("newEntries %o", newEntries); + const mergedEntries = [...allButLastPrevEntries, ...newEntries]; + index.current = mergedEntries.length; + console.log("mergedEntries %o", mergedEntries); + console.log("index.current %o", index.current); + + return mergedEntries; + }); + + } + setMessageObj({ isError: null, status: null, message: null }); setNextFetch(new Date().getTime() + fetchIntervalMs); } catch (error) { @@ -97,7 +111,7 @@ function Start() { return ( <> - {console.info("entries %o", entries)} + {console.info("entries %o", entries)}
    {messageObj.isError && diff --git a/src/models/entry.ts b/src/models/entry.ts index e7fc107..02b0bd8 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -21,6 +21,7 @@ export const entry = { } const entries = fileObj.content.entries; const lastEntry = fileObj.content.entries.at(-1); + let previousEntry = fileObj.content.entries.at(-1); // potentially overwritten if entry is set to ignore const entry = {} as Models.IEntry; entry.altitude = Number(req.query.altitude); @@ -32,11 +33,27 @@ export const entry = { entry.user = req.query.user as string; entry.ignore = false; - if (lastEntry) { // so there is a previous entry - entry.time = getTime(Number(req.query.timestamp), lastEntry); - lastEntry.ignore = getIgnore(lastEntry, entry); - entry.angle = getAngle(lastEntry, entry); - entry.distance = getDistance(entry, lastEntry) + if (lastEntry && previousEntry) { + entry.time = getTime(Number(req.query.timestamp), lastEntry); // time data is needed for ignore calculation + + if (entries.length > 1) { // the very first entry one shall never be ignored + lastEntry.ignore = getIgnore(lastEntry, entry); + } else { + lastEntry.ignore = false; + } + + if (lastEntry.ignore) { // rectify or replace previousEntry with last non ignored element + for (let i = entries.length - 1; i >= 0; i--) { + if (!entries[i].ignore) { + previousEntry = entries[i]; + break; + } + } + } + + entry.time = getTime(Number(req.query.timestamp), previousEntry); // overwrite time in case previousEnty was changed + entry.angle = getAngle(previousEntry, entry); + entry.distance = getDistance(entry, previousEntry) entry.speed = getSpeed(Number(req.query.speed), entry); } else { entry.angle = undefined; @@ -46,7 +63,7 @@ export const entry = { if (entries.length >= 1000) { logger.log(`File over 1000 lines: ${fileObj.path}`); - if (entry.hdop < 12 || (lastEntry && entry.hdop < lastEntry.hdop)) { + if (entry.hdop < 12 || (previousEntry && entry.hdop < previousEntry.hdop)) { entries[entries.length - 1] = entry; // replace last entry } } else { diff --git a/src/scripts/ignore.ts b/src/scripts/ignore.ts index 3e92857..d16bb1c 100644 --- a/src/scripts/ignore.ts +++ b/src/scripts/ignore.ts @@ -4,10 +4,10 @@ export function getIgnore(lastEntry: Models.IEntry, entry: Models.IEntry): boole const timing = Math.max(lastEntry.time.diff, entry.time.diff) - // Threshold increases with older previous entries or farther future entries. + // Threshold increases with older previous entries if (timing > 32) { threshold += Math.min(lastEntry.time.diff / 60, maxThreshold); } - + return lastEntry.hdop > threshold; } \ No newline at end of file diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index 2696d06..e79be6f 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -195,16 +195,23 @@ describe("GET /write", () => { it('check ignore', async () => { let jsonData = getData(filePath); - let entry = jsonData.entries[1]; - const lastEntry = jsonData.entries[0]; + let entry = jsonData.entries.at(-1); + let firstEntry = jsonData.entries[0]; + let previousEntry = null; - expect(entry.ignore).toBe(false); // current one to be false allways - expect(lastEntry.ignore).toBe(true); // last one to high hdop to be true + expect(entry.ignore).toBe(false); // current one to be false always + expect(firstEntry.ignore).toBe(false); // start entry to be false always await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET"); + jsonData = getData(filePath); - entry = jsonData.entries[1]; // same data point, but not last now therefore ignore true - expect(entry.ignore).toBe(true); + entry = jsonData.entries.at(-1); + previousEntry = jsonData.entries.at(-2); + firstEntry = jsonData.entries[0]; + + expect(entry.ignore).toBe(false); // current one to be false always + expect(firstEntry.ignore).toBe(false); // start entry to be false always + expect(previousEntry.ignore).toBe(true); // now since there is 3 entries the previous can be ignored }); }); diff --git a/views/index.ejs b/views/index.ejs index 6ccb8bf..b194ff6 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -6,10 +6,10 @@ LOREX - Osmand Webtracking Frontend - - + + - + From 4efcbf4dd567220c68d1d882a4632041280ce6ad Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Jul 2024 20:17:41 +0200 Subject: [PATCH 152/206] [Task] #94, add logging if logical error with ignore --- src/models/entry.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/models/entry.ts b/src/models/entry.ts index 02b0bd8..34dbb75 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -49,6 +49,11 @@ export const entry = { break; } } + + if (previousEntry === fileObj.content.entries.at(-1)) { + logger.error("previousEntry was not replaced"); + } + } entry.time = getTime(Number(req.query.timestamp), previousEntry); // overwrite time in case previousEnty was changed From 5e26a192a30a7e90bb0bd932385ce21b41be2340 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Jul 2024 20:18:54 +0200 Subject: [PATCH 153/206] [Task] #94, cleanup console.logs --- src/client/pages/Start.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 25cf726..8d91287 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -77,11 +77,8 @@ function Start() { if (newEntries.length > 1) { setEntries((prevEntries) => { const allButLastPrevEntries = prevEntries.slice(0, prevEntries.length -1); - console.log("newEntries %o", newEntries); const mergedEntries = [...allButLastPrevEntries, ...newEntries]; index.current = mergedEntries.length; - console.log("mergedEntries %o", mergedEntries); - console.log("index.current %o", index.current); return mergedEntries; }); @@ -111,7 +108,6 @@ function Start() { return ( <> - {console.info("entries %o", entries)}
    {messageObj.isError && From 7a55e4d8b19c0b149948aff25f09892bb9792f14 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 19 Jul 2024 21:04:54 +0200 Subject: [PATCH 154/206] [Fix] #93, offline message improvement (#96) --- src/client/pages/Start.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 8d91287..5a4bd91 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -88,11 +88,21 @@ function Start() { setMessageObj({ isError: null, status: null, message: null }); setNextFetch(new Date().getTime() + fetchIntervalMs); } catch (error) { + console.log("error fetching data %o", error); + + if (!error.response) { + setMessageObj({ isError: true, status: 499, message: error.message || "offline" }); + setNextFetch(new Date().getTime() + fetchIntervalMs); + return; + } + + if (error.response.status == 403) { setLogin(false) } + + setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }); + clearInterval(intervalID.current); intervalID.current = null; console.info("cleared Interval"); setNextFetch(null); - if (error.response.status == 403) { setLogin(false) } - setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }); } }; From 9e01a1c215aa69c4abfce1938ada739833d19d9d Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 15 Aug 2024 11:03:01 +0200 Subject: [PATCH 155/206] fix: package.json & package-lock.json to reduce vulnerabilities (#101) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-AXIOS-7361793 Co-authored-by: snyk-bot --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 054cfd3..1e77681 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.20", - "axios": "^1.7.2", + "axios": "^1.7.4", "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", @@ -3320,9 +3320,9 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", diff --git a/package.json b/package.json index a3849f3..0ce8da0 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.20", - "axios": "^1.7.2", + "axios": "^1.7.4", "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", From 50d8c1d70c697e4fb3f79e5cee4848d7a2e1235a Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 15 Aug 2024 11:04:32 +0200 Subject: [PATCH 156/206] fix: upgrade @mui/material from 5.15.20 to 5.16.5 (#102) Snyk has created this PR to upgrade @mui/material from 5.15.20 to 5.16.5. See this package in npm: @mui/material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 124 ++++++++++++---------------------------------- 1 file changed, 31 insertions(+), 93 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e77681..17de58e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1012,40 +1012,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.1.tgz", - "integrity": "sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==", - "dependencies": { - "@floating-ui/utils": "^0.2.0" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", - "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", - "dependencies": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.9.tgz", - "integrity": "sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", - "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -1667,41 +1633,10 @@ "node": ">=6" } }, - "node_modules/@mui/base": { - "version": "5.0.0-beta.40", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", - "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", - "@popperjs/core": "^2.11.8", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.4.tgz", - "integrity": "sha512-rNdHXhclwjEZnK+//3SR43YRx0VtjdHnUFhMSGYmAMJve+KiwEja/41EYh8V3pZKqF2geKyfcFUenTfDTYUR4w==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", + "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -1735,22 +1670,22 @@ } }, "node_modules/@mui/material": { - "version": "5.15.20", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.20.tgz", - "integrity": "sha512-tVq3l4qoXx/NxUgIx/x3lZiPn/5xDbdTE8VrLczNpfblLYZzlrbxA7kb9mI8NoBF6+w9WE9IrxWnKK5KlPI2bg==", + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.5.tgz", + "integrity": "sha512-eQrjjg4JeczXvh/+8yvJkxWIiKNHVptB/AqpsKfZBWp5mUD5U3VsjODMuUl1K2BSq0omV3CiO/mQmWSSMKSmaA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.40", - "@mui/core-downloads-tracker": "^5.15.20", - "@mui/system": "^5.15.20", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.20", + "@mui/core-downloads-tracker": "^5.16.5", + "@mui/system": "^5.16.5", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.5", + "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^18.2.0", + "react-is": "^18.3.1", "react-transition-group": "^4.4.5" }, "engines": { @@ -1780,13 +1715,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.4.tgz", - "integrity": "sha512-ZsAm8cq31SJ37SVWLRlu02v9SRthxnfQofaiv14L5Bht51B0dz6yQEoVU/V8UduZDCCIrWkBHuReVfKhE/UuXA==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", + "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.16.4", + "@mui/utils": "^5.16.6", "prop-types": "^15.8.1" }, "engines": { @@ -1807,9 +1742,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.4.tgz", - "integrity": "sha512-0+mnkf+UiAmTVB8PZFqOhqf729Yh0Cxq29/5cA3VAyDVTRIUUQ8FXQhiAhUIbijFmM72rY80ahFPXIm4WDbzcA==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", + "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", @@ -1839,16 +1774,16 @@ } }, "node_modules/@mui/system": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.4.tgz", - "integrity": "sha512-ET1Ujl2/8hbsD611/mqUuNArMCGv/fIWO/f8B3ZqF5iyPHM2aS74vhTNyjytncc4i6dYwGxNk+tLa7GwjNS0/w==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", + "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.16.4", - "@mui/styled-engine": "^5.16.4", + "@mui/private-theming": "^5.16.6", + "@mui/styled-engine": "^5.16.6", "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.4", + "@mui/utils": "^5.16.6", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1893,12 +1828,13 @@ } }, "node_modules/@mui/utils": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.4.tgz", - "integrity": "sha512-nlppYwq10TBIFqp7qxY0SvbACOXeOjeVL3pOcDsK0FT8XjrEXh9/+lkg8AEIzD16z7YfiJDQjaJG2OLkE7BxNg==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", + "@mui/types": "^7.2.15", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1960,6 +1896,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -3782,6 +3719,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } From 0de8b24b0270a185da958c071d95071220f58ec0 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 15 Aug 2024 11:56:28 +0200 Subject: [PATCH 157/206] 93 fix error message when server not available (#103) * [Fix] #93, offline message improvement * [Task] #93, removed background in status module when no data is present * [Task] #61, add cut class to map for styling * [Fix] #93 fix tests, be more specific on url, and let test fail non silently when csrf is not found --- src/client/components/Status.tsx | 2 +- src/client/css/start.css | 4 ++++ src/client/pages/Start.tsx | 4 ++-- src/tests/login.test.ts | 6 ++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/client/components/Status.tsx b/src/client/components/Status.tsx index 5c1991e..7d5026e 100644 --- a/src/client/components/Status.tsx +++ b/src/client/components/Status.tsx @@ -50,7 +50,7 @@ function Map({ entries }: { entries: Models.IEntry[] }) {
    • datapoints: {entries.length - statusData.ignoredEntries}({statusData.ignoredEntries})
    • Ø upload: {statusData.uploadMean}s
    • -
    • Ø speed: GPS: {statusData.speedGPSMean}km/h Calc: {statusData.speedCalcMean == "NaN" ? " - " : statusData.speedCalcMean }km/h
    • +
    • Ø speed: GPS: {statusData.speedGPSMean}km/h Calc: {statusData.speedCalcMean == "NaN" ? " - " : statusData.speedCalcMean}km/h
    ) diff --git a/src/client/css/start.css b/src/client/css/start.css index b197310..67b3530 100644 --- a/src/client/css/start.css +++ b/src/client/css/start.css @@ -57,6 +57,10 @@ position: relative; z-index: 0; + &.emptyData::after { + content: none; + } + &::after { content: ""; position: absolute; diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 5a4bd91..cbc6ecf 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -144,9 +144,9 @@ function Start() {
    -
    +
    -
    +
    image1
    image2
    diff --git a/src/tests/login.test.ts b/src/tests/login.test.ts index 4bb5699..e7a593e 100644 --- a/src/tests/login.test.ts +++ b/src/tests/login.test.ts @@ -25,7 +25,7 @@ describe('Login', () => { try { response = await axios({ method: "post", - url: "/login/csrf", + url: "http://localhost:80/login/csrf", headers: { "content-type": "application/x-www-form-urlencoded", "x-requested-with": "XMLHttpRequest" @@ -34,6 +34,7 @@ describe('Login', () => { serverStatus = response.status; } catch (error) { console.error(error); + throw Error("fail"); } @@ -79,7 +80,8 @@ describe('Login', () => { it('test invalid credentials to return error', async () => { try { - userDataWithToken.csrfToken = csrfToken + userDataWithToken.csrfToken = csrfToken; + console.log("csrfToken %o", userDataWithToken.csrfToken); await axios.post('http://localhost:80/login', qs.stringify(userDataWithToken)); } catch (error) { const axiosError = error as AxiosError; From 6e87449dd094368a7286ef09deeeacf38b0749c5 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 19 Aug 2024 13:09:52 +0200 Subject: [PATCH 158/206] [Fix] #94, repair overwriting the last data point --- src/client/components/Map.tsx | 25 +++++++++++++++---------- src/client/css/map.module.css | 3 --- src/client/pages/Start.tsx | 31 +++++++++++++++++-------------- 3 files changed, 32 insertions(+), 27 deletions(-) delete mode 100644 src/client/css/map.module.css diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index abfe558..c6e8eaf 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -1,10 +1,10 @@ import React, { useEffect } from 'react' -import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet' +import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet' import 'leaflet/dist/leaflet.css'; import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css'; // Re-uses images from ~leaflet package // import L from 'leaflet'; import 'leaflet-defaulticon-compatibility'; -import * as css from "../css/map.module.css"; +import "../css/map.css"; // Used to recenter the map to new coordinates @@ -27,20 +27,25 @@ function Map({ entries }: { entries: Models.IEntry[] }) { const lastEntry = entries.at(-1); const cleanEntries = entries.filter((entry) => !entry.ignore); + return ( - + - {cleanEntries.map((entry) => - - -
    {JSON.stringify(entry, null, 2)}
    -
    -
    - )} + {cleanEntries.map((entry) => { + console.log(entry.index); + return ( + + +
    {JSON.stringify(entry, null, 2)}
    +
    +
    + ) + })} +
    ) diff --git a/src/client/css/map.module.css b/src/client/css/map.module.css deleted file mode 100644 index 59c6bb8..0000000 --- a/src/client/css/map.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.mapContainer { - height: 100%; -} \ No newline at end of file diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index cbc6ecf..b761b73 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -70,36 +70,39 @@ function Start() { const newEntries = response.data.entries; - if (newEntries.length == 1) { - setEntries(newEntries); - } - - if (newEntries.length > 1) { + if (newEntries.length) { setEntries((prevEntries) => { - const allButLastPrevEntries = prevEntries.slice(0, prevEntries.length -1); - const mergedEntries = [...allButLastPrevEntries, ...newEntries]; - index.current = mergedEntries.length; + let allButLastPrevEntries, mergedEntries = []; + + if (prevEntries.length) { + allButLastPrevEntries = prevEntries.slice(0, prevEntries.length - 1); + mergedEntries = [...allButLastPrevEntries, ...newEntries]; + } else { + mergedEntries = newEntries; + } + index.current = mergedEntries.length; + return mergedEntries; }); - - } + + } setMessageObj({ isError: null, status: null, message: null }); setNextFetch(new Date().getTime() + fetchIntervalMs); } catch (error) { console.log("error fetching data %o", error); - + if (!error.response) { setMessageObj({ isError: true, status: 499, message: error.message || "offline" }); setNextFetch(new Date().getTime() + fetchIntervalMs); return; } - + if (error.response.status == 403) { setLogin(false) } - + setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }); - + clearInterval(intervalID.current); intervalID.current = null; console.info("cleared Interval"); setNextFetch(null); From 008e24bb23d3456eab2c3d426656c7f84c6101e4 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 21 Aug 2024 21:33:56 +0200 Subject: [PATCH 159/206] fix: upgrade @emotion/react from 11.11.4 to 11.13.0 (#104) Snyk has created this PR to upgrade @emotion/react from 11.11.4 to 11.13.0. See this package in npm: @emotion/react See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 126 ++++++++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 78 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17de58e..30e3851 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "lorex", "version": "0.0.1", "dependencies": { - "@emotion/react": "^11.11.4", + "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.20", @@ -788,15 +788,16 @@ } }, "node_modules/@emotion/babel-plugin": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", - "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", + "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/serialize": "^1.1.2", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.2.0", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", @@ -805,6 +806,12 @@ "stylis": "4.2.0" } }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -819,21 +826,29 @@ } }, "node_modules/@emotion/cache": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", - "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", - "dependencies": { - "@emotion/memoize": "^0.8.1", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", + "version": "11.13.1", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", + "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.2", @@ -849,17 +864,18 @@ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/react": { - "version": "11.11.4", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", - "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.0.tgz", + "integrity": "sha512-WkL+bw1REC2VNV1goQyfxjx1GYJkcc23CRQkXX+vZNLINyfI7o+uUn/rTGPt/xJ3bJHd5GcljgnxHf4wRw5VWQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.11.0", - "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", + "@emotion/babel-plugin": "^11.12.0", + "@emotion/cache": "^11.13.0", + "@emotion/serialize": "^1.3.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { @@ -872,21 +888,29 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", - "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.0.tgz", + "integrity": "sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==", + "license": "MIT", "dependencies": { - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/unitless": "^0.8.1", - "@emotion/utils": "^1.2.1", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.9.0", + "@emotion/utils": "^1.4.0", "csstype": "^3.0.2" } }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, "node_modules/@emotion/sheet": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" }, "node_modules/@emotion/styled": { "version": "11.11.5", @@ -911,27 +935,31 @@ } }, "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.9.0.tgz", + "integrity": "sha512-TP6GgNZtmtFaFcsOgExdnfxLLpRDla4Q66tnenA9CktvVSdNKDvMVuUah4QvWPIpNjrWsGg3qeGo9a43QooGZQ==", + "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", - "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "license": "MIT", "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@emotion/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz", + "integrity": "sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==", + "license": "MIT" }, "node_modules/@emotion/weak-memoize": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", - "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", diff --git a/package.json b/package.json index 0ce8da0..600f050 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@emotion/react": "^11.11.4", + "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.20", From b232179d3de44598c472b49a5c91ae7c41013a4c Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 21 Aug 2024 21:35:33 +0200 Subject: [PATCH 160/206] fix: upgrade @emotion/styled from 11.11.5 to 11.13.0 (#105) Snyk has created this PR to upgrade @emotion/styled from 11.11.5 to 11.13.0. See this package in npm: @emotion/styled See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 33 +++++++++++++++++++++------------ package.json | 1 + 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30e3851..4ae60d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "lorex", "version": "0.0.1", "dependencies": { + "@emotion/styled": "^11.13.0", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.20", @@ -851,13 +852,20 @@ "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", - "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz", + "integrity": "sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==", + "license": "MIT", "dependencies": { - "@emotion/memoize": "^0.8.1" + "@emotion/memoize": "^0.9.0" } }, + "node_modules/@emotion/is-prop-valid/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, "node_modules/@emotion/memoize": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", @@ -913,16 +921,17 @@ "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "11.11.5", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", - "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", + "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.11.0", - "@emotion/is-prop-valid": "^1.2.2", - "@emotion/serialize": "^1.1.4", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", - "@emotion/utils": "^1.2.1" + "@emotion/babel-plugin": "^11.12.0", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0" }, "peerDependencies": { "@emotion/react": "^11.0.0-rc.0", diff --git a/package.json b/package.json index 600f050..9a4c738 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { + "@emotion/styled": "^11.13.0", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.20", From ee33d4b2aa9e0e174ddb22ac683b04a1d1e276b3 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 21 Aug 2024 21:35:46 +0200 Subject: [PATCH 161/206] fix: upgrade react-router-dom from 6.25.0 to 6.25.1 (#106) Snyk has created this PR to upgrade react-router-dom from 6.25.0 to 6.25.1. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ae60d8..cbca266 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-leaflet": "^4.2.1", - "react-router-dom": "^6.24.0", + "react-router-dom": "^6.25.1", "toobusy-js": "^0.5.1" }, "devDependencies": { @@ -11507,9 +11507,9 @@ } }, "node_modules/react-router": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.0.tgz", - "integrity": "sha512-bziKjCcDbcxgWS9WlWFcQIVZ2vJHnCP6DGpQDT0l+0PFDasfJKgzf9CM22eTyhFsZkjk8ApCdKjJwKtzqH80jQ==", + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", + "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==", "license": "MIT", "dependencies": { "@remix-run/router": "1.18.0" @@ -11522,13 +11522,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.0.tgz", - "integrity": "sha512-BhcczgDWWgvGZxjDDGuGHrA8HrsSudilqTaRSBYLWDayvo1ClchNIDVt5rldqp6e7Dro5dEFx9Mzc+r292lN0w==", + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz", + "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", "license": "MIT", "dependencies": { "@remix-run/router": "1.18.0", - "react-router": "6.25.0" + "react-router": "6.25.1" }, "engines": { "node": ">=14.0.0" diff --git a/package.json b/package.json index 9a4c738..c6cd2a3 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-leaflet": "^4.2.1", - "react-router-dom": "^6.24.0", + "react-router-dom": "^6.25.1", "toobusy-js": "^0.5.1" }, "_moduleAliases": { From 87afbd37fe741e83a8f02d7a89de8629c9e3dcc7 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 21 Aug 2024 21:36:28 +0200 Subject: [PATCH 162/206] fix: upgrade @mui/icons-material from 5.16.4 to 5.16.5 (#107) Snyk has created this PR to upgrade @mui/icons-material from 5.16.4 to 5.16.5. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index cbca266..c08194e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@emotion/styled": "^11.13.0", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.20", + "@mui/icons-material": "^5.16.5", "@mui/material": "^5.15.20", "axios": "^1.7.4", "bcrypt": "^5.1.1", @@ -1681,9 +1681,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.4.tgz", - "integrity": "sha512-j9/CWctv6TH6Dou2uR2EH7UOgu79CW/YcozxCYVLJ7l03pCsiOlJ5sBArnWJxJ+nGkFwyL/1d1k8JEPMDR125A==", + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.5.tgz", + "integrity": "sha512-bn88xxU/J9UV0s6+eutq7o3TTOrOlbCX+KshFb8kxgIxJZZfYz3JbAXVMivvoMF4Md6jCVUzM9HEkf4Ajab4tw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" diff --git a/package.json b/package.json index c6cd2a3..a13038c 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@emotion/styled": "^11.13.0", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.20", + "@mui/icons-material": "^5.16.5", "@mui/material": "^5.15.20", "axios": "^1.7.4", "bcrypt": "^5.1.1", From f14c9e373987a931ce5123339351671d76a73e72 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 21 Aug 2024 21:36:44 +0200 Subject: [PATCH 163/206] fix: upgrade express-rate-limit from 7.3.1 to 7.4.0 (#108) Snyk has created this PR to upgrade express-rate-limit from 7.3.1 to 7.4.0. See this package in npm: express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c08194e..235b152 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "dotenv": "^16.4.5", "ejs": "^3.1.10", "express": "^4.19.2", - "express-rate-limit": "^7.3.0", + "express-rate-limit": "^7.4.0", "express-slow-down": "^2.0.3", "express-validator": "^7.1.0", "helmet": "^7.1.0", @@ -5321,9 +5321,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.1.tgz", - "integrity": "sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.0.tgz", + "integrity": "sha512-v1204w3cXu5gCDmAvgvzI6qjzZzoMWKnyVDk3ACgfswTQLYiGen+r8w0VnXnGMmzEN/g8fwIQ4JrFFd4ZP6ssg==", "license": "MIT", "engines": { "node": ">= 16" diff --git a/package.json b/package.json index a13038c..ef6452b 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "dotenv": "^16.4.5", "ejs": "^3.1.10", "express": "^4.19.2", - "express-rate-limit": "^7.3.0", + "express-rate-limit": "^7.4.0", "express-slow-down": "^2.0.3", "express-validator": "^7.1.0", "helmet": "^7.1.0", From 842d0c805ca96ed14c8b471bba57df0b80cb9b7a Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 21 Aug 2024 21:45:51 +0200 Subject: [PATCH 164/206] 109 marker and line design (#110) * [Task] #109, start polyline * [Task] #94, marker * [Task] #109, gradient color polyline color based on speed * [Task] #109 linter fixes --- httpdocs/css/base.css | 5 ++ httpdocs/images/marker.svg | 4 ++ package-lock.json | 47 ++++++++++++- package.json | 5 ++ src/client/components/Map.tsx | 117 +++++++++++++++++++++++++++++---- src/client/css/map.css | 37 +++++++++++ src/client/tsconfig.json | 2 +- src/client/types_polyline.d.ts | 14 ++++ 8 files changed, 216 insertions(+), 15 deletions(-) create mode 100644 httpdocs/images/marker.svg create mode 100644 src/client/css/map.css create mode 100644 src/client/types_polyline.d.ts diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index fd4fe7f..2041853 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -130,6 +130,8 @@ Neutral: #131211 --text: color-mix(in oklch, var(--neutral) 20%, black); --textOnColor: var(--neutral); --semiBg: #ffffffbb; + --contrastText: white; + --contrastBackground: black; --baseFontWeightModifier: 50; @@ -148,6 +150,9 @@ Neutral: #131211 --text: color-mix(in oklch, var(--neutral) 20%, white); --semiBg: #00000077; + --contrastText: black; + --contrastBackground: white; + --baseFontWeightModifier: -50; } } diff --git a/httpdocs/images/marker.svg b/httpdocs/images/marker.svg new file mode 100644 index 0000000..da4c914 --- /dev/null +++ b/httpdocs/images/marker.svg @@ -0,0 +1,4 @@ + + Marker Arrow + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 235b152..8ed1d87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,12 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.5", "@mui/material": "^5.15.20", + "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.4", "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", + "culori": "^4.0.1", "dotenv": "^16.4.5", "ejs": "^3.1.10", "express": "^4.19.2", @@ -28,6 +30,8 @@ "jsonwebtoken": "^9.0.2", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.2", + "leaflet-polycolor": "^2.0.5", + "leaflet-rotatedmarker": "^0.2.0", "module-alias": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -40,6 +44,7 @@ "@tsconfig/node20": "^20.1.4", "@types/bcrypt": "^5.0.2", "@types/compression": "^1.7.5", + "@types/culori": "^2.1.1", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", @@ -2090,6 +2095,13 @@ "@types/node": "*" } }, + "node_modules/@types/culori": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/culori/-/culori-2.1.1.tgz", + "integrity": "sha512-NzLYD0vNHLxTdPp8+RlvGbR2NfOZkwxcYGFwxNtm+WH2NuUNV8785zv1h0sulFQ5aFQ9n/jNDUuJeo3Bh7+oFA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "8.56.7", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz", @@ -2144,7 +2156,6 @@ "version": "7946.0.14", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", - "dev": true, "license": "MIT" }, "node_modules/@types/graceful-fs": { @@ -2230,12 +2241,20 @@ "version": "1.9.12", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.12.tgz", "integrity": "sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==", - "dev": true, "license": "MIT", "dependencies": { "@types/geojson": "*" } }, + "node_modules/@types/leaflet-rotatedmarker": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@types/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.5.tgz", + "integrity": "sha512-GaKK1bdQ6NYGkVdZj2cHe8Eu1BVf40Jhtmd8pZj5gQSJcTy5iTog0hsMIhf6QQDKnaEgrRJzm4OES6B9vxi4dw==", + "license": "MIT", + "dependencies": { + "@types/leaflet": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4072,6 +4091,15 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/culori": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/culori/-/culori-4.0.1.tgz", + "integrity": "sha512-LSnjA6HuIUOlkfKVbzi2OlToZE8OjFi667JWN9qNymXVXzGDmvuP60SSgC+e92sd7B7158f7Fy3Mb6rXS5EDPw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -7493,6 +7521,21 @@ "integrity": "sha512-IrKagWxkTwzxUkFIumy/Zmo3ksjuAu3zEadtOuJcKzuXaD76Gwvg2Z1mLyx7y52ykOzM8rAH5ChBs4DnfdGa6Q==", "license": "BSD-2-Clause" }, + "node_modules/leaflet-polycolor": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/leaflet-polycolor/-/leaflet-polycolor-2.0.5.tgz", + "integrity": "sha512-nksE5PlCgzULil8rDzGfOnVH1o62GKyT4oLFyaqXUEidwcCMDvKr7x4DTCDdpUjiaoOLYBZTKwCT2XA0bfgExQ==", + "license": "MIT", + "dependencies": { + "leaflet": "^1.9.2" + } + }, + "node_modules/leaflet-rotatedmarker": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz", + "integrity": "sha512-yc97gxLXwbZa+Gk9VCcqI0CkvIBC9oNTTjFsHqq4EQvANrvaboib4UdeQLyTnEqDpaXHCqzwwVIDHtvz2mUiDg==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", diff --git a/package.json b/package.json index ef6452b..d04e595 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@tsconfig/node20": "^20.1.4", "@types/bcrypt": "^5.0.2", "@types/compression": "^1.7.5", + "@types/culori": "^2.1.1", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", @@ -67,10 +68,12 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.5", "@mui/material": "^5.15.20", + "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.4", "bcrypt": "^5.1.1", "chalk": "^4.1.2", "compression": "^1.7.4", + "culori": "^4.0.1", "dotenv": "^16.4.5", "ejs": "^3.1.10", "express": "^4.19.2", @@ -82,6 +85,8 @@ "jsonwebtoken": "^9.0.2", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.2", + "leaflet-polycolor": "^2.0.5", + "leaflet-rotatedmarker": "^0.2.0", "module-alias": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index c6e8eaf..bc8aeb3 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -1,22 +1,72 @@ import React, { useEffect } from 'react' -import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet' +import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet' +import leafletPolycolor from 'leaflet-polycolor'; +import { formatRgb, toGamut, parse, Oklch } from 'culori'; +import L, { LatLngExpression } from 'leaflet'; +import 'leaflet-rotatedmarker'; import 'leaflet/dist/leaflet.css'; -import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css'; // Re-uses images from ~leaflet package -// import L from 'leaflet'; -import 'leaflet-defaulticon-compatibility'; import "../css/map.css"; +leafletPolycolor(L); // Used to recenter the map to new coordinates const MapRecenter = ({ lat, lon, zoom }: { lat: number, lon: number, zoom: number }) => { const map = useMap(); - useEffect(() => { // Fly to that coordinates and set new zoom level map.flyTo([lat, lon], zoom); }, [lat, lon]); return null; +}; + +const MultiColorPolyline = ({ cleanEntries }: { cleanEntries: Models.IEntry[] }) => { + const map = useMap(); + const useRelativeColors = true; // Change candidate; Use color in range to maximum speed, like from 0 to max, rather than fixed range + + function calculateHue(baseHue, maxSpeed, currentSpeed) { + // range of currentSpeed and maxSpeed transfered to range from 0 to 360 + const hueOffset = (currentSpeed / maxSpeed) * 360; + // add baseHue to the hueOffset and overflow at 360 + const hue = (baseHue + hueOffset) % 360; + + return hue; + } + + useEffect(() => { + if (map) { + let maxSpeed = 0; + + if (useRelativeColors) { + maxSpeed = cleanEntries.reduce((maxSpeed, entry) => { + // compare the current entry's GPS speed with the maxSpeed found so far + return Math.max(maxSpeed, entry.speed.gps); + }, cleanEntries[0].speed.gps); + maxSpeed *= 3.6; // convert M/S to KM/h + } + + const colorsArray = cleanEntries.map((entry) => { + const startColor = parse('oklch(62.8% 0.2577 29.23)') as Oklch; // red + const currentSpeed = entry.speed.gps * 3.6; // convert to km/h + + startColor.h = calculateHue(startColor.h, maxSpeed, currentSpeed); + startColor.l = currentSpeed > maxSpeed * 0.8 ? startColor.l = currentSpeed / maxSpeed : startColor.l; + + const rgbInGamut = toGamut('rgb', 'oklch', null)(startColor); // map OKLCH to the RGB gamut + const colorRgb = formatRgb(rgbInGamut); // format the result as an RGB string + return colorRgb; + }); + + const polylineArray: LatLngExpression[] = cleanEntries.map((entry) => ([entry.lat, entry.lon])); + + L.polycolor(polylineArray, { + colors: colorsArray, + weight: 5 + }).addTo(map); + } + }, [map]); + + return null; }; function Map({ entries }: { entries: Models.IEntry[] }) { @@ -28,25 +78,68 @@ function Map({ entries }: { entries: Models.IEntry[] }) { const cleanEntries = entries.filter((entry) => !entry.ignore); + // Function to create custom icon with dynamic className + function createCustomIcon(entry: Models.IEntry) { + let className = ""; + let iconSize = 15; + if (entry.index == 0) { + className = "start" + } + if (entry == lastEntry) { + className = "end" + } + + if (className) { + iconSize = 22; + } + + return L.divIcon({ + html: ` + + Marker Arrow + + `, + shadowUrl: null, + shadowSize: null, + shadowAnchor: null, + iconSize: [iconSize, iconSize], + iconAnchor: [iconSize / 2, iconSize / 2], + popupAnchor: [0, 0], + className: `customMarkerIcon ${className}`, + }); + } + return ( - + {cleanEntries.map((entry) => { - console.log(entry.index); return ( - - -
    {JSON.stringify(entry, null, 2)}
    -
    -
    +
    + + +
    {JSON.stringify(entry, null, 2)}
    +
    +
    + + +
    ) })} + + +
    ) } diff --git a/src/client/css/map.css b/src/client/css/map.css new file mode 100644 index 0000000..8bf913d --- /dev/null +++ b/src/client/css/map.css @@ -0,0 +1,37 @@ +.mapContainer { + height: 100%; +} + + +.leaflet-popup-content { + font-size: 1.2rem; + min-width: min-content; +} + +.leaflet-overlay-pane canvas { /* polyline */ + filter: drop-shadow(0px 0px 3px var(--neutral)); +} + + +.customMarkerIcon { + + &.start, &.end { + display: flex; + place-content: center; + border: 2px solid var(--contrastBackground); + outline: 3px solid var(--contrastBackground); + outline-offset: 3px; + border-radius: 50%; + background: var(--contrastBackground); + + svg { + height: 80%; + } + } + + &.start { + outline: none; + } + + +} \ No newline at end of file diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index 7777c58..953d117 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -6,5 +6,5 @@ "jsx": "react", "esModuleInterop": true }, - "include": ["**/*.tsx", "**/*.ts", "types.d.ts", "../../types.d.ts"] + "include": ["**/*.tsx", "**/*.ts", "types.d.ts", "../../types.d.ts", "types_polyline.d.ts"] } \ No newline at end of file diff --git a/src/client/types_polyline.d.ts b/src/client/types_polyline.d.ts new file mode 100644 index 0000000..f955d35 --- /dev/null +++ b/src/client/types_polyline.d.ts @@ -0,0 +1,14 @@ +// Polyline plugin for native Leaflet +import * as L from 'leaflet'; + +declare module 'leaflet' { + namespace Polycolor { + interface Options { + colors: Array; + weight?: number; + } + } + + // Declare the actual polycolor function under the L namespace + function polycolor(latlngs: L.LatLngExpression[], options: Polycolor.Options): L.Polyline; +} From 5775d84bd59a2930eb34ebc69501ab2ed41513b9 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 26 Aug 2024 11:02:21 +0200 Subject: [PATCH 165/206] fix: upgrade react-router-dom from 6.25.1 to 6.26.0 (#113) Snyk has created this PR to upgrade react-router-dom from 6.25.1 to 6.26.0. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 32 +++++++++++++------------------- package.json | 2 +- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8ed1d87..00402bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "lorex", "version": "0.0.1", "dependencies": { - "@emotion/styled": "^11.13.0", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.5", @@ -36,7 +35,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-leaflet": "^4.2.1", - "react-router-dom": "^6.25.1", + "react-router-dom": "^6.26.0", "toobusy-js": "^0.5.1" }, "devDependencies": { @@ -871,11 +870,6 @@ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", "license": "MIT" }, - "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, "node_modules/@emotion/react": { "version": "11.13.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.0.tgz", @@ -1955,9 +1949,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", - "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.0.tgz", + "integrity": "sha512-zDICCLKEwbVYTS6TjYaWtHXxkdoUvD/QXvyVZjGCsWz5vyH7aFeONlPffPdW+Y/t6KT0MgXb2Mfjun9YpWN1dA==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -11550,12 +11544,12 @@ } }, "node_modules/react-router": { - "version": "6.25.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", - "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==", + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.0.tgz", + "integrity": "sha512-wVQq0/iFYd3iZ9H2l3N3k4PL8EEHcb0XlU2Na8nEwmiXgIUElEH6gaJDtUQxJ+JFzmIXaQjfdpcGWaM6IoQGxg==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.18.0" + "@remix-run/router": "1.19.0" }, "engines": { "node": ">=14.0.0" @@ -11565,13 +11559,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.25.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz", - "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.0.tgz", + "integrity": "sha512-RRGUIiDtLrkX3uYcFiCIxKFWMcWQGMojpYZfcstc63A1+sSnVgILGIm9gNUA6na3Fm1QuPGSBQH2EMbAZOnMsQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.18.0", - "react-router": "6.25.1" + "@remix-run/router": "1.19.0", + "react-router": "6.26.0" }, "engines": { "node": ">=14.0.0" diff --git a/package.json b/package.json index d04e595..63d842b 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-leaflet": "^4.2.1", - "react-router-dom": "^6.25.1", + "react-router-dom": "^6.26.0", "toobusy-js": "^0.5.1" }, "_moduleAliases": { From 6773f8d81e2ac1da7d315d1fd90aa871331915a1 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 26 Aug 2024 11:02:32 +0200 Subject: [PATCH 166/206] fix: upgrade @mui/material from 5.16.5 to 5.16.6 (#112) Snyk has created this PR to upgrade @mui/material from 5.16.5 to 5.16.6. See this package in npm: @mui/material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00402bb..f4b0017 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.5", - "@mui/material": "^5.15.20", + "@mui/material": "^5.16.6", "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.4", "bcrypt": "^5.1.1", @@ -1706,16 +1706,16 @@ } }, "node_modules/@mui/material": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.5.tgz", - "integrity": "sha512-eQrjjg4JeczXvh/+8yvJkxWIiKNHVptB/AqpsKfZBWp5mUD5U3VsjODMuUl1K2BSq0omV3CiO/mQmWSSMKSmaA==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.6.tgz", + "integrity": "sha512-0LUIKBOIjiFfzzFNxXZBRAyr9UQfmTAFzbt6ziOU2FDXhorNN2o3N9/32mNJbCA8zJo2FqFU6d3dtoqUDyIEfA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.16.5", - "@mui/system": "^5.16.5", + "@mui/core-downloads-tracker": "^5.16.6", + "@mui/system": "^5.16.6", "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.5", + "@mui/utils": "^5.16.6", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", diff --git a/package.json b/package.json index 63d842b..ac8ad40 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.5", - "@mui/material": "^5.15.20", + "@mui/material": "^5.16.6", "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.4", "bcrypt": "^5.1.1", From e2df714e706666543a91a25f78860aced4372036 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 26 Aug 2024 11:12:30 +0200 Subject: [PATCH 167/206] Switch polyline (#114) * [Revert] #109 remove polyColor Plugin * [Fix, MultiLine] #109, refactor coloring lines; while fetchinng new data vs reloading MaxSpeed might change the more entries are fetched. Example Testcase and after 6 entries are there, reload and see colors change ... well not with this fix. * [Task] #109, improve polyline display, remove unused code --- package.json | 1 - src/client/components/Map.tsx | 82 ++++++++++++----------------- src/client/tsconfig.json | 2 +- src/client/types_polyline.d.ts | 14 ----- src/testData/createTestData.test.ts | 2 +- 5 files changed, 37 insertions(+), 64 deletions(-) delete mode 100644 src/client/types_polyline.d.ts diff --git a/package.json b/package.json index ac8ad40..e017a68 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@emotion/styled": "^11.13.0", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.5", diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index bc8aeb3..20ca1c8 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -1,8 +1,8 @@ -import React, { useEffect } from 'react' -import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet' +import React, { useEffect, useState } from 'react' +import { MapContainer, Marker, Polyline, Popup, TileLayer, useMap } from 'react-leaflet' import leafletPolycolor from 'leaflet-polycolor'; -import { formatRgb, toGamut, parse, Oklch } from 'culori'; -import L, { LatLngExpression } from 'leaflet'; +import { toGamut, parse, Oklch, formatCss } from 'culori'; +import L from 'leaflet'; import 'leaflet-rotatedmarker'; import 'leaflet/dist/leaflet.css'; import "../css/map.css"; @@ -19,55 +19,47 @@ const MapRecenter = ({ lat, lon, zoom }: { lat: number, lon: number, zoom: numbe return null; }; + + const MultiColorPolyline = ({ cleanEntries }: { cleanEntries: Models.IEntry[] }) => { - const map = useMap(); - const useRelativeColors = true; // Change candidate; Use color in range to maximum speed, like from 0 to max, rather than fixed range + const [useRelativeColors] = useState(true); // Change candidate; Use color in range to maximum speed, like from 0 to max, rather than fixed range - function calculateHue(baseHue, maxSpeed, currentSpeed) { + let maxSpeed = 0; + const startColor = parse('oklch(62.8% 0.2577 29.23)') as Oklch; // red + const calculateHue = function (baseHue, maxSpeed, currentSpeed) { // range of currentSpeed and maxSpeed transfered to range from 0 to 360 const hueOffset = (currentSpeed / maxSpeed) * 360; // add baseHue to the hueOffset and overflow at 360 - const hue = (baseHue + hueOffset) % 360; - - return hue; + return (baseHue + hueOffset) % 360; } - useEffect(() => { - if (map) { - let maxSpeed = 0; - - if (useRelativeColors) { - maxSpeed = cleanEntries.reduce((maxSpeed, entry) => { - // compare the current entry's GPS speed with the maxSpeed found so far - return Math.max(maxSpeed, entry.speed.gps); - }, cleanEntries[0].speed.gps); - maxSpeed *= 3.6; // convert M/S to KM/h - } - - const colorsArray = cleanEntries.map((entry) => { - const startColor = parse('oklch(62.8% 0.2577 29.23)') as Oklch; // red - const currentSpeed = entry.speed.gps * 3.6; // convert to km/h + if (useRelativeColors) { + maxSpeed = cleanEntries.reduce((maxSpeed, entry) => { + // compare the current entry's GPS speed with the maxSpeed found so far + return Math.max(maxSpeed, entry.speed.gps); + }, cleanEntries[0].speed.gps); + maxSpeed *= 3.6; // convert M/S to KM/h + } - startColor.h = calculateHue(startColor.h, maxSpeed, currentSpeed); - startColor.l = currentSpeed > maxSpeed * 0.8 ? startColor.l = currentSpeed / maxSpeed : startColor.l; + return cleanEntries.map((entry, index) => { + if (!index) { return false; } + const previousEntry = cleanEntries[index - 1]; + const color = startColor; + const currentSpeed = entry.speed.gps * 3.6; // convert to km/h - const rgbInGamut = toGamut('rgb', 'oklch', null)(startColor); // map OKLCH to the RGB gamut - const colorRgb = formatRgb(rgbInGamut); // format the result as an RGB string + color.h = calculateHue(color.h, maxSpeed, currentSpeed); + color.l = currentSpeed > maxSpeed * 0.75 ? color.l = currentSpeed / maxSpeed : color.l; - return colorRgb; - }); + const correctedColor = toGamut('rgb', 'oklch', null)(color); // map OKLCH to the RGB gamut - const polylineArray: LatLngExpression[] = cleanEntries.map((entry) => ([entry.lat, entry.lon])); - L.polycolor(polylineArray, { - colors: colorsArray, - weight: 5 - }).addTo(map); - } - }, [map]); - - return null; -}; + return () + }); +} function Map({ entries }: { entries: Models.IEntry[] }) { if (!entries?.length) { @@ -110,7 +102,7 @@ function Map({ entries }: { entries: Models.IEntry[] }) { } return ( - + {JSON.stringify(entry, null, 2)} - -
    ) })} - - - + ) diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index 953d117..7777c58 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -6,5 +6,5 @@ "jsx": "react", "esModuleInterop": true }, - "include": ["**/*.tsx", "**/*.ts", "types.d.ts", "../../types.d.ts", "types_polyline.d.ts"] + "include": ["**/*.tsx", "**/*.ts", "types.d.ts", "../../types.d.ts"] } \ No newline at end of file diff --git a/src/client/types_polyline.d.ts b/src/client/types_polyline.d.ts deleted file mode 100644 index f955d35..0000000 --- a/src/client/types_polyline.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Polyline plugin for native Leaflet -import * as L from 'leaflet'; - -declare module 'leaflet' { - namespace Polycolor { - interface Options { - colors: Array; - weight?: number; - } - } - - // Declare the actual polycolor function under the L namespace - function polycolor(latlngs: L.LatLngExpression[], options: Polycolor.Options): L.Polyline; -} diff --git a/src/testData/createTestData.test.ts b/src/testData/createTestData.test.ts index 39b0c6c..2335567 100644 --- a/src/testData/createTestData.test.ts +++ b/src/testData/createTestData.test.ts @@ -48,7 +48,7 @@ describe('test Data', () => { const lat = (start.lat + (diff.lat / (entries - 1) * i)).toFixed(8); const lon = (start.lon + (diff.lon / (entries - 1) * i)).toFixed(8); setTimeout(async () => { - await callServer(undefined, `user=xx&lat=${lat}&lon=${lon}×tamp=R3Pl4C3&hdop=${Math.floor(Math.random() * 15) + 1}&altitude=${i+1}&speed=${35.5 + i}&heading=${262 + Math.floor(Math.random() * 20) - 10}&key=test`, 200, "GET"); + await callServer(undefined, `user=xx&lat=${lat}&lon=${lon}×tamp=R3Pl4C3&hdop=${Math.floor(Math.random() * 15) + 1}&altitude=${i+1}&speed=${38 + i*2}&heading=${262 + Math.floor(Math.random() * 20) - 10}&key=test`, 200, "GET"); console.log("called server " + (i + 1) + "/" + entries); }, 1000 * 30 * i); From 7fb42c0415f73fe2b96d5a1e3447b9b75a6a2e7e Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 26 Aug 2024 11:38:01 +0200 Subject: [PATCH 168/206] [Task] #77, change timing to round up, so it "feels" more accurate --- httpdocs/css/base.css | 2 +- src/client/pages/Start.tsx | 33 ++++++++++++++++----------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index 2041853..9b094ff 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -206,7 +206,7 @@ Neutral: #131211 font-weight: calc(800 + var(--baseFontWeightModifier)); } -.fade { animation: fade 1s 1s forwards; } +.fade { animation: fade 1s 2s forwards; } .fadeIn { animation: reverse fade 1s forwards; } @keyframes fade { to { diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index b761b73..8618bd5 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -17,22 +17,21 @@ function timeAgo(timestamp: number): string { const diffInSeconds = Math.floor((now - timestamp) / 1000); const seconds = diffInSeconds; - const minutes = Math.floor(diffInSeconds / 60); - const hours = Math.floor(diffInSeconds / 3600); - const days = Math.floor(diffInSeconds / 86400); - const weeks = Math.floor(diffInSeconds / 604800); - const months = Math.floor(diffInSeconds / 2592000); - const years = Math.floor(diffInSeconds / 31536000); - - if (seconds < 8) { return "Instant"; } - else if (seconds < 30) { return "Just now" } - else if (seconds < 60) { return "a moment ago" } - else if (minutes < 60) { return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`; } - else if (hours < 24) { return `${hours} ${hours === 1 ? "hour" : "hours"} ago`; } - else if (days < 7) { return `${days} ${days === 1 ? "day" : "days"} ago`; } - else if (weeks < 4) { return `${weeks} ${weeks === 1 ? "week" : "weeks"} ago`; } - else if (months < 12) { return `${months} ${months === 1 ? "month" : "months"} ago`; } - else { return `${years} ${years === 1 ? "year" : "years"} ago`; } + const minutes = Math.round(diffInSeconds / 60); + const hours = Math.round(diffInSeconds / 3600); + const days = Math.round(diffInSeconds / 86400); + const months = Math.round(diffInSeconds / 2592000); + const years = Math.round(diffInSeconds / 31536000); + + if (seconds < 8) return "Instant"; + else if (seconds < 25) return "Just now"; + else if (seconds < 50) return "a moment ago"; + else if (minutes < 60) return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`; + else if (hours < 24) return `${hours} ${hours === 1 ? "hour" : "hours"} ago`; + else if (days < 30) return `${days} ${days === 1 ? "day" : "days"} ago`; + else if (months < 12) return `${months} ${months === 1 ? "month" : "months"} ago`; + else return `${years} ${years === 1 ? "year" : "years"} ago`; + } function Start() { @@ -80,7 +79,7 @@ function Start() { } else { mergedEntries = newEntries; } - + index.current = mergedEntries.length; return mergedEntries; From bfc34409a8b3a9b17503b60b6acbc0beea2414b8 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 26 Aug 2024 15:37:10 +0200 Subject: [PATCH 169/206] [Task] #115, remove SVG Animation on startup based on media Query --- httpdocs/css/base.css | 2 ++ src/client/components/App.tsx | 3 +-- src/client/components/removeSvgAnimation.tsx | 28 ++++++++++++++++++++ src/client/index.tsx | 1 + src/client/pages/Login.tsx | 2 +- views/index.ejs | 8 +++--- 6 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 src/client/components/removeSvgAnimation.tsx diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index 9b094ff..6cb634f 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -226,6 +226,8 @@ Neutral: #131211 animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; } + + /* note this does not remove SVG Animations */ } /* development */ diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 0a05fac..2cbebea 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -47,5 +47,4 @@ const App = () => { ); } -export default App; - +export default App; \ No newline at end of file diff --git a/src/client/components/removeSvgAnimation.tsx b/src/client/components/removeSvgAnimation.tsx new file mode 100644 index 0000000..60ef9c4 --- /dev/null +++ b/src/client/components/removeSvgAnimation.tsx @@ -0,0 +1,28 @@ +document.addEventListener('DOMContentLoaded', () => { + const mq = window.matchMedia('(prefers-reduced-motion: reduce), (update: slow)'); + + // -- create small SVG to inject to see when SVG animations are rendered and started -- + const svgTemplate = ``; + + const container = document.createElement('div'); + container.innerHTML = svgTemplate; + + const animateElement = container.querySelector('animate'); + animateElement.addEventListener('beginEvent', () => { + if (mq.matches) { + const animations = document.querySelectorAll('animate, animateTransform'); + animations.forEach(animationElement => { + (animationElement as SVGAnimateElement).endElement(); + }); + } + }); + + + // Append the SVG directly to the document body + document.body.appendChild(container.firstElementChild as SVGElement); + +}); \ No newline at end of file diff --git a/src/client/index.tsx b/src/client/index.tsx index 1293afa..9030437 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,3 +1,4 @@ +import "./components/removeSvgAnimation"; import * as React from 'react'; import { Root, createRoot } from 'react-dom/client'; import { Experimental_CssVarsProvider as CssVarsProvider, experimental_extendTheme as extendTheme } from '@mui/material/styles'; diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index 00a8ee5..ba405b8 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { TextField, Button, InputAdornment, CircularProgress } from '@mui/material'; import { AccountCircle, Lock, HighlightOff, Login as LoginIcon, Check } from '@mui/icons-material'; import "../css/login.css"; diff --git a/views/index.ejs b/views/index.ejs index b194ff6..ff3d550 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -36,7 +36,7 @@
  • datapoints: {entries.length - statusData.ignoredEntries}({statusData.ignoredEntries})
  • Ø upload: {statusData.uploadMean}s
  • Ø speed: GPS: {statusData.speedGPSMean}km/h Calc: {statusData.speedCalcMean == "NaN" ? " - " : statusData.speedCalcMean}km/h
  • -
  • +
  • maxSpeed: {statusData.maxSpeed}
  • +
  • vertcial: {statusData.verticalCalc[0]} up, {statusData.verticalCalc[1]} down
  • +
  • distance {statusData.distance}km
) } diff --git a/src/client/helper/maxSpeed.ts b/src/client/helper/maxSpeed.ts new file mode 100644 index 0000000..7477af6 --- /dev/null +++ b/src/client/helper/maxSpeed.ts @@ -0,0 +1,7 @@ +export const getMaxSpeed = function (cleanEntries:Models.IEntry[]) { + let maxSpeed = cleanEntries.reduce((maxSpeed, entry:Models.IEntry) => { + // compare the current entry's GPS speed with the maxSpeed found so far + return Math.max(maxSpeed, entry.speed.gps); + }, cleanEntries[0].speed.gps); + return maxSpeed *= 3.6; // convert M/S to KM/h +}; \ No newline at end of file From 11d760a4e576cfc3ff5d898267b2ff45175e4c9e Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 26 Aug 2024 20:26:17 +0200 Subject: [PATCH 172/206] [Task] #77, improve example test data --- src/testData/createTestData.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testData/createTestData.test.ts b/src/testData/createTestData.test.ts index 10ac06c..8838124 100644 --- a/src/testData/createTestData.test.ts +++ b/src/testData/createTestData.test.ts @@ -48,7 +48,7 @@ describe('test Data', () => { const lat = (start.lat + (diff.lat / (entries - 1) * i)).toFixed(8); const lon = (start.lon + (diff.lon / (entries - 1) * i)).toFixed(8); setTimeout(async () => { - await callServer(undefined, `user=xx&lat=${lat}&lon=${lon}×tamp=R3Pl4C3&hdop=${Math.floor(Math.random() * 15) + 1}&altitude=${i+1}&speed=${38 + i*2.5}&heading=${262 + Math.floor(Math.random() * 20) - 10}&key=test`, 200, "GET"); + await callServer(undefined, `user=xx&lat=${lat}&lon=${lon}×tamp=R3Pl4C3&hdop=${Math.floor(Math.random() * 15) + 1}&altitude=${i+1}&speed=${39 + i*2.5}&heading=${262 + Math.floor(Math.random() * 20) - 10}&key=test`, 200, "GET"); console.log("called server " + (i + 1) + "/" + entries); }, 1000 * 30 * i); From 14573d68077fc5a456ae20bf13b6274779a6d6b3 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 27 Aug 2024 11:06:20 +0200 Subject: [PATCH 173/206] [Task] #109, line dashed when diff is high, marker start icon when diff is higher --- src/client/components/Map.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index 30b40c6..ea751b1 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -49,11 +49,17 @@ const MultiColorPolyline = ({ cleanEntries }: { cleanEntries: Models.IEntry[] }) const correctedColor = toGamut('rgb', 'oklch', null)(color); // map OKLCH to the RGB gamut + let strokeDashArray = null; + if (entry.time.diff > 100) { strokeDashArray = "4 8";} return () }); } @@ -71,7 +77,7 @@ function Map({ entries }: { entries: Models.IEntry[] }) { function createCustomIcon(entry: Models.IEntry) { let className = ""; let iconSize = 15; - if (entry.index == 0) { + if (entry.index == 0 || entry.time.diff >= 300) { className = "start" } if (entry == lastEntry) { From 70ff0d9f5f86e09df5be2c53123de6083adacad4 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 27 Aug 2024 15:37:37 +0200 Subject: [PATCH 174/206] [Task] #77, status design --- src/client/components/Status.tsx | 74 ++++++++++++++++++++++++++------ src/client/css/start.css | 2 +- src/client/css/status.css | 56 ++++++++++++++++++++++++ src/client/css/status.module.css | 0 4 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 src/client/css/status.css delete mode 100644 src/client/css/status.module.css diff --git a/src/client/components/Status.tsx b/src/client/components/Status.tsx index c93395b..421f510 100644 --- a/src/client/components/Status.tsx +++ b/src/client/components/Status.tsx @@ -1,6 +1,13 @@ import React from 'react' import { getMaxSpeed } from "../helper/maxSpeed"; -//import * as css from "../css/status.module.css"; +import "../css/status.css"; +import StorageIcon from '@mui/icons-material/Storage'; +import NetworkCheckIcon from '@mui/icons-material/NetworkCheck'; +import SpeedIcon from '@mui/icons-material/Speed'; +import BoltIcon from '@mui/icons-material/Bolt'; +import ShowChartIcon from '@mui/icons-material/ShowChart'; +import EastIcon from '@mui/icons-material/East'; + function getStatusData(entries) { const cleanEntries = entries.filter((entry: Models.IEntry) => !entry.ignore); @@ -44,15 +51,15 @@ function getStatusData(entries) { } } - return [up, down]; + return [(up / 1000).toFixed(2), (down / 1000).toFixed(2)]; } function getDistance() { - return cleanEntries.reduce((accumulatorValue:number, entry) => { + return cleanEntries.reduce((accumulatorValue: number, entry) => { console.log(accumulatorValue); - if (!entry.distance ) { return accumulatorValue } + if (!entry.distance) { return accumulatorValue } return accumulatorValue + parseFloat(entry.distance.horizontal); - }, 0) / 1000; + }, 0) / 1000; } const ignoredEntries = entries.length - cleanEntries.length; @@ -82,14 +89,55 @@ function Map({ entries }: { entries: Models.IEntry[] }) { //const lastEntry = entries.at(-1); return ( -
    -
  • datapoints: {entries.length - statusData.ignoredEntries}({statusData.ignoredEntries})
  • -
  • Ø upload: {statusData.uploadMean}s
  • -
  • Ø speed: GPS: {statusData.speedGPSMean}km/h Calc: {statusData.speedCalcMean == "NaN" ? " - " : statusData.speedCalcMean}km/h
  • -
  • maxSpeed: {statusData.maxSpeed}
  • -
  • vertcial: {statusData.verticalCalc[0]} up, {statusData.verticalCalc[1]} down
  • -
  • distance {statusData.distance}km
  • -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
data + {entries.length - statusData.ignoredEntries}({statusData.ignoredEntries}) +
Ø upload + {statusData.uploadMean}s +
Ø speed + GPS: {statusData.speedGPSMean}km/h Calc: {statusData.speedCalcMean == "NaN" ? " - " : statusData.speedCalcMean}km/h +
maxSpeed + {statusData.maxSpeed}km/h +
vertical + {statusData.verticalCalc[0]}km up, {statusData.verticalCalc[1]}km down +
Distance + {statusData.distance}km +
) } diff --git a/src/client/css/start.css b/src/client/css/start.css index 67b3530..6b3c25a 100644 --- a/src/client/css/start.css +++ b/src/client/css/start.css @@ -19,7 +19,7 @@ /* grid layout */ height: 100%; display: grid; - grid-template-columns: 1fr 40vmin; + grid-template-columns: 1fr minmax(18.5rem, 40vmin); grid-template-rows: minmax(3em, auto) 1fr 1fr 1fr minmax(3em, auto); .grid-item { diff --git a/src/client/css/status.css b/src/client/css/status.css new file mode 100644 index 0000000..fae036d --- /dev/null +++ b/src/client/css/status.css @@ -0,0 +1,56 @@ +.status { + container-type: inline-size; +} + + +.statusTable { + font-size: 1.4rem; + padding: 0.7rem 0.2em 0.2em 1em; + height: 100%; + + th { + text-align: left; + padding: 0 1rem; + } + + td { + font-stretch: 120%; + font-weight: 350; + font-style: oblique 10deg; + letter-spacing: 0.0125em; + } + + .strike { + font-style: normal; + position: relative; + + &::after { + content: ""; + position: absolute; + top: 40%; + left: 60%; + transform: translate(-50%, -50%); + border-top: 1px solid; + width: 100%; + } + } + + span { + white-space: nowrap; + } +} + +@container (max-width: 30rem) { + .statusTable { + padding: 0.5rem 0.2rem; + font-size: 1.3rem; + } + + th { + display: none; + } + + td { + padding-inline: 0.2rem; + } +} \ No newline at end of file diff --git a/src/client/css/status.module.css b/src/client/css/status.module.css deleted file mode 100644 index e69de29..0000000 From 53684242bdf46d01b86a034eada2507163c2740b Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 12:59:25 +0200 Subject: [PATCH 175/206] [Task] #83, forced scheme for map --- httpdocs/css/base.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index 6cb634f..58ec796 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -119,7 +119,7 @@ Danger: #ff0000 Success: #59ec04 Neutral: #131211 */ -:root { +:root, [data-mui-color-scheme="light"] { --main: oklch(77.2% 0.1738 64.55); --info: oklch(50% 0.2838 268.01); --alert: oklch(62.5% 0.2577 29.23); @@ -132,6 +132,8 @@ Neutral: #131211 --semiBg: #ffffffbb; --contrastText: white; --contrastBackground: black; + --semiContrastBackground: #00000077; + --baseFontWeightModifier: 50; @@ -140,7 +142,7 @@ Neutral: #131211 accent-color: var(--main); /* dark theme, initial state (prefers mq) by react */ - &[data-mui-color-scheme="dark"] { + &[data-mui-color-scheme="dark"], [data-mui-color-scheme="dark"] { --main: oklch(75% 0.1738 64.55); --info: oklch(47.5% 0.2838 268.01); --alert: oklch(60% 0.2577 29.23); @@ -152,6 +154,7 @@ Neutral: #131211 --contrastText: black; --contrastBackground: white; + --semiContrastBackground: #ffffffbb; --baseFontWeightModifier: -50; } From 1292b3510f8a88a5c1752fc4430c07ed41cf4402 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 12:59:53 +0200 Subject: [PATCH 176/206] [Task] #83, change context, mode globally available --- src/client/components/App.tsx | 16 ++++++++++++---- src/client/components/ModeSwitcher.tsx | 19 +++++++------------ src/client/pages/Login.tsx | 6 +++--- src/client/pages/Start.tsx | 4 ++-- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 2cbebea..6229027 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,9 +1,11 @@ -import React, { createContext, useState } from 'react'; +import React, { createContext, useEffect, useState } from 'react'; import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { useColorScheme } from '@mui/material/styles'; +import { useMediaQuery } from '@mui/material'; import Start from '../pages/Start'; import Login from '../pages/Login'; -export const LoginContext = createContext([]); +export const Context = createContext([]); export function convertJwt() { const token = localStorage?.jwt; @@ -39,11 +41,17 @@ const router = createBrowserRouter([ const App = () => { const [userInfo, setUserInfo] = useState(convertJwt()); const [isLoggedIn, setLogin] = useState(loginDefault(userInfo)); + const { mode, setMode } = useColorScheme(); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + + useEffect(() => { + setMode(prefersDarkMode ? "dark" : "light"); + }, [prefersDarkMode, setMode]); return ( - + - + ); } diff --git a/src/client/components/ModeSwitcher.tsx b/src/client/components/ModeSwitcher.tsx index cf4bb31..f8f6523 100644 --- a/src/client/components/ModeSwitcher.tsx +++ b/src/client/components/ModeSwitcher.tsx @@ -1,18 +1,13 @@ -import React from 'react'; -import { useColorScheme } from '@mui/material/styles'; -import { Button, useMediaQuery } from '@mui/material'; +import React, { useContext} from 'react'; +import { Context } from "../components/App"; +import { Button } from '@mui/material'; import { LightMode, Nightlight } from '@mui/icons-material'; import * as css from "../css/modeSwticher.module.css"; function ModeSwitcher() { - const { mode, setMode } = useColorScheme(); - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - // run only once - React.useEffect(() => { - setMode(prefersDarkMode ? "dark" : "light"); - }, []); + const [, , , , mode, setMode] = useContext(Context); - return ( + return ( ); -}; +} export default ModeSwitcher; diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index ba405b8..2b2d9a7 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -1,11 +1,11 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useState } from 'react'; import { TextField, Button, InputAdornment, CircularProgress } from '@mui/material'; import { AccountCircle, Lock, HighlightOff, Login as LoginIcon, Check } from '@mui/icons-material'; import "../css/login.css"; import ModeSwitcher from '../components/ModeSwitcher'; import axios from 'axios'; import qs from 'qs'; -import { LoginContext, convertJwt } from '../components/App'; +import { Context, convertJwt } from '../components/App'; import { useNavigate } from 'react-router-dom'; import LinearBuffer from '../components/LinearBuffer'; @@ -13,7 +13,7 @@ function Login() { const [finish, setFinish] = useState(1); const [start, setStart] = useState(1); const navigate = useNavigate(); - const [isLoggedIn, setLogin, userInfo, setUserInfo] = useContext(LoginContext); + const [isLoggedIn, setLogin, userInfo, setUserInfo] = useContext(Context); const [formInfo, updateFormInfo] = useState({ user: { isError: false, diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 8618bd5..fe6a957 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useContext, useRef } from 'react' import "../css/start.css"; import axios from 'axios'; -import { LoginContext } from "../components/App"; +import { Context } from "../components/App"; import { HighlightOff, Check } from '@mui/icons-material'; import { Button } from '@mui/material'; import ModeSwitcher from '../components/ModeSwitcher'; @@ -35,7 +35,7 @@ function timeAgo(timestamp: number): string { } function Start() { - const [isLoggedIn, setLogin, userInfo] = useContext(LoginContext); + const [isLoggedIn, setLogin, userInfo] = useContext(Context); const [entries, setEntries] = useState([]); const [messageObj, setMessageObj] = useState({ isError: null, status: null, message: null }); const [lastFetch, setLastFetch] = useState(); From b6998074e120b1ff14aec1a00765f3849d084157 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 13:00:44 +0200 Subject: [PATCH 177/206] [Task] #83, react update, cluster install --- package-lock.json | 48 +++++++++++++++++++++++++++++++++-------------- package.json | 4 ++-- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4b0017..87064db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "lorex", "version": "0.0.1", "dependencies": { + "@changey/react-leaflet-markercluster": "^4.0.0-rc1", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.5", @@ -29,7 +30,6 @@ "jsonwebtoken": "^9.0.2", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.2", - "leaflet-polycolor": "^2.0.5", "leaflet-rotatedmarker": "^0.2.0", "module-alias": "^2.2.3", "react": "^18.3.1", @@ -50,7 +50,7 @@ "@types/jsonwebtoken": "^9.0.6", "@types/leaflet": "^1.9.12", "@types/node": "^20.11.30", - "@types/react": "^18.2.74", + "@types/react": "^18.3.4", "@types/react-dom": "^18.2.24", "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", @@ -771,6 +771,23 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@changey/react-leaflet-markercluster": { + "version": "4.0.0-rc1", + "resolved": "https://registry.npmjs.org/@changey/react-leaflet-markercluster/-/react-leaflet-markercluster-4.0.0-rc1.tgz", + "integrity": "sha512-gS1lEQiQwyeI6Y6Wuxuqqffwywm7giQw4tbcqtJP8zyT5bc3AzW2/EVJGwWORYo/PLDdDnvOrpI+lUJy2UA5MQ==", + "license": "MIT", + "dependencies": { + "@react-leaflet/core": "^2.0.0", + "leaflet": "^1.8.0", + "leaflet.markercluster": "^1.5.3", + "react-leaflet": "^4.0.0" + }, + "peerDependencies": { + "leaflet": "^1.8.0", + "leaflet.markercluster": "^1.5.3", + "react-leaflet": "^4.0.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2287,9 +2304,10 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.74", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.74.tgz", - "integrity": "sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==", + "version": "18.3.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", + "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -7515,21 +7533,21 @@ "integrity": "sha512-IrKagWxkTwzxUkFIumy/Zmo3ksjuAu3zEadtOuJcKzuXaD76Gwvg2Z1mLyx7y52ykOzM8rAH5ChBs4DnfdGa6Q==", "license": "BSD-2-Clause" }, - "node_modules/leaflet-polycolor": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/leaflet-polycolor/-/leaflet-polycolor-2.0.5.tgz", - "integrity": "sha512-nksE5PlCgzULil8rDzGfOnVH1o62GKyT4oLFyaqXUEidwcCMDvKr7x4DTCDdpUjiaoOLYBZTKwCT2XA0bfgExQ==", - "license": "MIT", - "dependencies": { - "leaflet": "^1.9.2" - } - }, "node_modules/leaflet-rotatedmarker": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz", "integrity": "sha512-yc97gxLXwbZa+Gk9VCcqI0CkvIBC9oNTTjFsHqq4EQvANrvaboib4UdeQLyTnEqDpaXHCqzwwVIDHtvz2mUiDg==", "license": "MIT" }, + "node_modules/leaflet.markercluster": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", + "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", + "license": "MIT", + "peerDependencies": { + "leaflet": "^1.3.1" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -11505,6 +11523,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -11516,6 +11535,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" diff --git a/package.json b/package.json index e017a68..18f368e 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@types/jsonwebtoken": "^9.0.6", "@types/leaflet": "^1.9.12", "@types/node": "^20.11.30", - "@types/react": "^18.2.74", + "@types/react": "^18.3.4", "@types/react-dom": "^18.2.24", "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", @@ -63,6 +63,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { + "@changey/react-leaflet-markercluster": "^4.0.0-rc1", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.5", @@ -84,7 +85,6 @@ "jsonwebtoken": "^9.0.2", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.2", - "leaflet-polycolor": "^2.0.5", "leaflet-rotatedmarker": "^0.2.0", "module-alias": "^2.2.3", "react": "^18.3.1", From c852365e07bc3403dcf0d4cb0baf1123780dc953 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 13:01:45 +0200 Subject: [PATCH 178/206] [Task, multiline] #83, map tilelayer Introduced new map TileLayers using layers control, and styled it Introduced markerClusterGroup from @changey, since others had issues like broken marker images, or lack for typescript support Refined dashed array styles Addapted context changes, to fetch mode globally, for seperate map theme Markers have none style if neither end or start, to be targeted Introduced Layer array for tilelayers When Layers are changed theme for map is set/updated --- src/client/components/Map.tsx | 174 ++++++++++++++++++++++++++-------- src/client/css/map.css | 32 +++++++ 2 files changed, 166 insertions(+), 40 deletions(-) diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index ea751b1..05d16a1 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -1,14 +1,14 @@ -import React, { useEffect, useState } from 'react' -import { MapContainer, Marker, Polyline, Popup, TileLayer, useMap } from 'react-leaflet' -import leafletPolycolor from 'leaflet-polycolor'; +import React, { useContext, useEffect, useState } from 'react' +import { Context } from "../components/App"; +import { LayersControl, MapContainer, Marker, Polyline, Popup, TileLayer, useMap, useMapEvents } from 'react-leaflet' import { toGamut, parse, Oklch, formatCss } from 'culori'; import L from 'leaflet'; import 'leaflet-rotatedmarker'; import 'leaflet/dist/leaflet.css'; import "../css/map.css"; import { getMaxSpeed } from "../helper/maxSpeed"; -leafletPolycolor(L); - +import MarkerClusterGroup from "@changey/react-leaflet-markercluster"; +import "@changey/react-leaflet-markercluster/dist/styles.min.css"; // Used to recenter the map to new coordinates const MapRecenter = ({ lat, lon, zoom }: { lat: number, lon: number, zoom: number }) => { @@ -20,8 +20,6 @@ const MapRecenter = ({ lat, lon, zoom }: { lat: number, lon: number, zoom: numbe return null; }; - - const MultiColorPolyline = ({ cleanEntries }: { cleanEntries: Models.IEntry[] }) => { const [useRelativeColors] = useState(true); // Change candidate; Use color in range to maximum speed, like from 0 to max, rather than fixed range @@ -51,15 +49,15 @@ const MultiColorPolyline = ({ cleanEntries }: { cleanEntries: Models.IEntry[] }) let strokeDashArray = null; - if (entry.time.diff > 100) { strokeDashArray = "4 8";} + if (entry.time.diff > 100) { strokeDashArray = "4 8"; } return () }); } @@ -68,15 +66,18 @@ function Map({ entries }: { entries: Models.IEntry[] }) { if (!entries?.length) { return No Data to be displayed } + const [, , , , mode] = useContext(Context); + const [mapStyle, setMapStyle] = useState(mode); const lastEntry = entries.at(-1); const cleanEntries = entries.filter((entry) => !entry.ignore); + const cleanEntriesWithoutLast = cleanEntries.slice(0, -1); // Function to create custom icon with dynamic className function createCustomIcon(entry: Models.IEntry) { - let className = ""; - let iconSize = 15; + let className = "none"; + let iconSize = 14; if (entry.index == 0 || entry.time.diff >= 300) { className = "start" } @@ -84,7 +85,7 @@ function Map({ entries }: { entries: Models.IEntry[] }) { className = "end" } - if (className) { + if (className != "none") { iconSize = 22; } @@ -104,34 +105,127 @@ function Map({ entries }: { entries: Models.IEntry[] }) { }); } + const layers = [ + { + name: "OSM DE", + attribution: '© OpenStreetMap contributors', + url: 'https://{s}.tile.openstreetmap.de/{z}/{x}/{y}.png', + markerStyle: mode + }, + { + name: "ArcGis WorldImagery", + attribution: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', + url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', + markerStyle: "dark" + }, + // { + // name: "OpenRailway", + // attribution: 'Map data: © OpenStreetMap contributors | Map style: © OpenStreetMap contributors © CARTO', + url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', + markerStyle: "light", + default: mode == "light" + }, + { + name: "Stadia AlidadeSmoothDark", + attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', + url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png', + markerStyle: "dark", + default: mode == "dark" + }, + { + name: "Stadia AlidadeSatelite", + attribution: '© CNES, Distribution Airbus DS, © Airbus DS, © PlanetObserver (Contains Copernicus Data) | © Stadia Maps © OpenMapTiles © OpenStreetMap contributors', + url: 'https://tiles.stadiamaps.com/tiles/alidade_satellite/{z}/{x}/{y}{r}.jpg', + markerStyle: "dark" + }, + { + name: "Mapbox Satelite Streets", + attribution: '© Mapbox', + url: 'https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoidHlwZS1zdHlsZSIsImEiOiJjbGJ4aG14enEwZ2toM3BvNW5uanhuOGRvIn0.7TUEM9vA-EYSt3WW_bcsAA', + markerStyle: "dark" + } + ] + + + // custom hook to handle map events and track active layer + // used to switch marker design + const LayerChangeHandler = () => { + useMapEvents({ + baselayerchange: (event) => { + const newLayer = layers.filter((layer) => layer.name == event.name); + console.log(newLayer); + if (newLayer[0].markerStyle != mapStyle) { + setMapStyle(newLayer[0].markerStyle); + } + }, + }); + return null; + }; + + return ( - - - - {cleanEntries.map((entry) => { - return ( -
- - -
{JSON.stringify(entry, null, 2)}
-
-
-
- ) - })} - - - -
+
+ + + + + {layers.map((layer, index) => { + return ( + + + + ) + })} + + + + {cleanEntriesWithoutLast.map((entry) => { + return ( + + +
{JSON.stringify(entry, null, 2)}
+
+
+ ) + })} +
+ + + {/* lastEntry */} + + +
{JSON.stringify(lastEntry, null, 2)}
+
+
+ + +
+
) } diff --git a/src/client/css/map.css b/src/client/css/map.css index 8bf913d..b2646a7 100644 --- a/src/client/css/map.css +++ b/src/client/css/map.css @@ -1,7 +1,23 @@ +.mapStyle { + display: contents; +} + .mapContainer { height: 100%; } +.leaflet-control-layers-base { + font-size: 1.4rem; + + label { + cursor: pointer; + margin-bottom: 0.3em; + } + + input { + top: 0; + } +} .leaflet-popup-content { font-size: 1.2rem; @@ -12,6 +28,18 @@ filter: drop-shadow(0px 0px 3px var(--neutral)); } +.marker-cluster-small[class] { /* overwrite default cluster style */ + background: none; + div { + background-color: var(--semiContrastBackground); + font-weight: bold; + box-shadow: inset 0 0 2px 2px var(--contrastBackground); + span { + color: var(--contrastText); + font-size: 1.5rem; + } + } +} .customMarkerIcon { @@ -33,5 +61,9 @@ outline: none; } + &.none { + filter: drop-shadow(0 0 5px var(--contrastBackground)); + } + } \ No newline at end of file From 76e54a2e3b85cbf93b1a79a54a35ff8f91dcd41d Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 13:45:04 +0200 Subject: [PATCH 179/206] [Task] #77 improve responsive design, hide images, status overlays map --- src/client/css/start.css | 15 ++++++++++++--- src/client/css/status.css | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/client/css/start.css b/src/client/css/start.css index 6b3c25a..59d81f2 100644 --- a/src/client/css/start.css +++ b/src/client/css/start.css @@ -19,7 +19,7 @@ /* grid layout */ height: 100%; display: grid; - grid-template-columns: 1fr minmax(18.5rem, 40vmin); + grid-template-columns: 1fr minmax(16rem, 40vmin); grid-template-rows: minmax(3em, auto) 1fr 1fr 1fr minmax(3em, auto); .grid-item { @@ -48,6 +48,11 @@ margin-right: 1em; background-color: color-mix(in oklab, transparent 50%, var(--main)); + + @media (max-width: 35em) { + grid-column: 1 / -1; + margin-right: 0; + } } &.status { @@ -86,6 +91,9 @@ display: grid; overflow: auto; + @media (max-width: 35em) { + display: none; + } } .image { @@ -102,10 +110,11 @@ } &.subinfo { - grid-column: 1; + grid-column: 1 / -1; padding: 0.5em 0.8em; - @media (min-width: 30em) { + @media (min-width: 35em) { padding: 0.7em 2em; + grid-column: 1; } .MuiLinearProgress-root { diff --git a/src/client/css/status.css b/src/client/css/status.css index fae036d..07747e0 100644 --- a/src/client/css/status.css +++ b/src/client/css/status.css @@ -53,4 +53,18 @@ td { padding-inline: 0.2rem; } +} + +@container (max-width: 20rem) { + .statusTable { + padding: 0.3rem 0.1rem; + font-size: 1.15rem; + } + + td { + padding: 0 0 0 0.1rem; + font-stretch: 100%; + font-weight: 400; + letter-spacing: normal; + } } \ No newline at end of file From 35055b3120e17442ce8344fc4a9c065acff13397 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 17:22:55 +0200 Subject: [PATCH 180/206] [Task] #77, adjust coloring and opacity, used for status --- httpdocs/css/base.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index 58ec796..85d43b8 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -150,7 +150,7 @@ Neutral: #131211 --bg: color-mix(in oklch, var(--neutral) 20%, black); --text: color-mix(in oklch, var(--neutral) 20%, white); - --semiBg: #00000077; + --semiBg: #00000055; --contrastText: black; --contrastBackground: white; From c5eb790aabb572f1a3dfe57f112729ede2a036be Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 17:24:10 +0200 Subject: [PATCH 181/206] [Task] #83, adjust imports --- src/client/components/Status.tsx | 102 +++++++++++---------- src/client/{helper => scripts}/maxSpeed.ts | 0 2 files changed, 52 insertions(+), 50 deletions(-) rename src/client/{helper => scripts}/maxSpeed.ts (100%) diff --git a/src/client/components/Status.tsx b/src/client/components/Status.tsx index 421f510..6dd5c88 100644 --- a/src/client/components/Status.tsx +++ b/src/client/components/Status.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { getMaxSpeed } from "../helper/maxSpeed"; +import { getMaxSpeed } from "../scripts/maxSpeed"; import "../css/status.css"; import StorageIcon from '@mui/icons-material/Storage'; import NetworkCheckIcon from '@mui/icons-material/NetworkCheck'; @@ -81,7 +81,7 @@ function getStatusData(entries) { } } -function Map({ entries }: { entries: Models.IEntry[] }) { +function Status({ entries }: { entries: Models.IEntry[] }) { if (!entries?.length) { return No Data to be displayed } @@ -90,55 +90,57 @@ function Map({ entries }: { entries: Models.IEntry[] }) { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
data - {entries.length - statusData.ignoredEntries}({statusData.ignoredEntries}) -
Ø upload - {statusData.uploadMean}s -
Ø speed - GPS: {statusData.speedGPSMean}km/h Calc: {statusData.speedCalcMean == "NaN" ? " - " : statusData.speedCalcMean}km/h -
maxSpeed - {statusData.maxSpeed}km/h -
vertical - {statusData.verticalCalc[0]}km up, {statusData.verticalCalc[1]}km down -
Distance - {statusData.distance}km -
data + {entries.length - statusData.ignoredEntries}({statusData.ignoredEntries}) +
Ø upload + {statusData.uploadMean}s +
Ø speed + GPS: {statusData.speedGPSMean}km/h Calc: {statusData.speedCalcMean == "NaN" ? " - " : statusData.speedCalcMean}km/h +
maxSpeed + {statusData.maxSpeed}km/h +
vertical + {statusData.verticalCalc[0]}km up, {statusData.verticalCalc[1]}km down +
Distance + {statusData.distance}km +
) } -export default Map +export default Status diff --git a/src/client/helper/maxSpeed.ts b/src/client/scripts/maxSpeed.ts similarity index 100% rename from src/client/helper/maxSpeed.ts rename to src/client/scripts/maxSpeed.ts From e14ffd7c544c360c91a3942a1bd1fdf8d13bb08a Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 17:24:50 +0200 Subject: [PATCH 182/206] [Task] #77, changed responsive design for mobile --- src/client/css/start.css | 55 +++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/src/client/css/start.css b/src/client/css/start.css index 59d81f2..b68fa16 100644 --- a/src/client/css/start.css +++ b/src/client/css/start.css @@ -29,6 +29,7 @@ width: 100%; justify-content: space-between; padding: 0.5em 0.8em; + @media (min-width: 30em) { padding: 0.7em 2em; } @@ -46,9 +47,9 @@ grid-column: 1; grid-row: 2 / span 3; margin-right: 1em; - + background-color: color-mix(in oklab, transparent 50%, var(--main)); - + @media (max-width: 35em) { grid-column: 1 / -1; margin-right: 0; @@ -76,13 +77,14 @@ --shadowColor: var(--text); filter: url(#rough-light); - box-shadow: 0 0 0.2em var(--shadowColor); + box-shadow: 0 0 0.2em var(--shadowColor); } + [data-mui-color-scheme="dark"] &::after { --shadowColor: var(--main); filter: url(#rough-light) drop-shadow(0 3px 5px var(--shadowColor)); } - + } &.images { @@ -91,6 +93,7 @@ display: grid; overflow: auto; + @media (max-width: 35em) { display: none; } @@ -98,20 +101,21 @@ .image { display: inline-block; - aspect-ratio: 16/9; - background-color: moccasin; - } + aspect-ratio: 16/9; + margin-bottom: 0.5rem; + cursor: pointer; - .image+.image { - background-color: lightgoldenrodyellow; - } - .image+.image+.image { - background-color: antiquewhite; + > * { + width: 100%; + height: 100%; + } } + &.subinfo { grid-column: 1 / -1; padding: 0.5em 0.8em; + @media (min-width: 35em) { padding: 0.7em 2em; grid-column: 1; @@ -119,17 +123,19 @@ .MuiLinearProgress-root { margin: -0.5em 0 1em -0.8em; + @media (min-width: 30em) { margin: -0.7em -1em 1em -2em; - } + } } .info { display: inline-block; padding-inline: 1em; border-right: 0.1rem solid; - - &:last-child, &.noDivider { + + &:last-child, + &.noDivider { border: none; padding-right: 0; } @@ -158,7 +164,11 @@ .title { font-size: 1.1em; - @media (min-width: 30em) { font-size: inherit; } + + @media (min-width: 30em) { + font-size: inherit; + } + width: 100%; text-align: center; } @@ -166,7 +176,11 @@ .loginButton { color: white; - [data-mui-color-scheme="dark"] & { color: black;} + + [data-mui-color-scheme="dark"] & { + color: black; + } + margin-left: auto; cursor: pointer; white-space: nowrap; @@ -177,23 +191,28 @@ @media (min-width: 30em) { /* reset to MUI default */ font-size: 1.3rem; - padding: 8px 22px; + padding: 8px 22px; } .MuiButton-icon { font-size: 1.3rem; + @media (min-width: 30em) { font-size: inherit; } } + .MuiButton-startIcon { margin-left: 6px; + @media (min-width: 30em) { margin-left: 0px; } } + .MuiButton-endIcon { margin-left: 0; + @media (min-width: 30em) { margin-left: 4px; } From 905ffcc5edb432c17d88feeaab3659e01b3939a4 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 17:25:32 +0200 Subject: [PATCH 183/206] [Task] #83, extracted to own module, added fly option --- src/client/components/MapCenter.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/client/components/MapCenter.tsx diff --git a/src/client/components/MapCenter.tsx b/src/client/components/MapCenter.tsx new file mode 100644 index 0000000..9847261 --- /dev/null +++ b/src/client/components/MapCenter.tsx @@ -0,0 +1,16 @@ +import { useEffect } from 'react' +import { useMap } from "react-leaflet"; + +// Used to recenter the map to new coordinates +export const MapRecenter = ({ lat, lon, zoom, fly }: { lat: number, lon: number, zoom: number, fly: boolean }) => { + const map = useMap(); + useEffect(() => { + // Fly to that coordinates and set new zoom level + if (fly) { + map.flyTo([lat, lon], zoom); + } else { + map.setView([lat, lon], zoom); + } + }, [lat, lon]); + return null; +}; \ No newline at end of file From d19cfcd6545c89d42d755a63b1896991d2deb9fa Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 17:26:54 +0200 Subject: [PATCH 184/206] [Task] #83, layers extracted, clickable minimap, corrected for mapbox tileSize --- src/client/components/Map.tsx | 73 +++++-------------------------- src/client/components/MiniMap.tsx | 33 ++++++++++++++ src/client/pages/Start.tsx | 11 +++-- src/client/scripts/layers.ts | 48 ++++++++++++++++++++ src/client/types.d.ts | 16 +++++++ 5 files changed, 115 insertions(+), 66 deletions(-) create mode 100644 src/client/components/MiniMap.tsx create mode 100644 src/client/scripts/layers.ts diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index 05d16a1..9882e0f 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react' +import React, { useContext, useState } from 'react' import { Context } from "../components/App"; import { LayersControl, MapContainer, Marker, Polyline, Popup, TileLayer, useMap, useMapEvents } from 'react-leaflet' import { toGamut, parse, Oklch, formatCss } from 'culori'; @@ -6,19 +6,11 @@ import L from 'leaflet'; import 'leaflet-rotatedmarker'; import 'leaflet/dist/leaflet.css'; import "../css/map.css"; -import { getMaxSpeed } from "../helper/maxSpeed"; +import { getMaxSpeed } from "../scripts/maxSpeed"; +import { layers } from "../scripts/layers"; import MarkerClusterGroup from "@changey/react-leaflet-markercluster"; import "@changey/react-leaflet-markercluster/dist/styles.min.css"; - -// Used to recenter the map to new coordinates -const MapRecenter = ({ lat, lon, zoom }: { lat: number, lon: number, zoom: number }) => { - const map = useMap(); - useEffect(() => { - // Fly to that coordinates and set new zoom level - map.flyTo([lat, lon], zoom); - }, [lat, lon]); - return null; -}; +import { MapRecenter } from "./MapCenter"; const MultiColorPolyline = ({ cleanEntries }: { cleanEntries: Models.IEntry[] }) => { const [useRelativeColors] = useState(true); // Change candidate; Use color in range to maximum speed, like from 0 to max, rather than fixed range @@ -74,6 +66,7 @@ function Map({ entries }: { entries: Models.IEntry[] }) { const cleanEntriesWithoutLast = cleanEntries.slice(0, -1); + // Function to create custom icon with dynamic className function createCustomIcon(entry: Models.IEntry) { let className = "none"; @@ -105,61 +98,12 @@ function Map({ entries }: { entries: Models.IEntry[] }) { }); } - const layers = [ - { - name: "OSM DE", - attribution: '© OpenStreetMap contributors', - url: 'https://{s}.tile.openstreetmap.de/{z}/{x}/{y}.png', - markerStyle: mode - }, - { - name: "ArcGis WorldImagery", - attribution: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', - url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', - markerStyle: "dark" - }, - // { - // name: "OpenRailway", - // attribution: 'Map data: © OpenStreetMap contributors | Map style: © OpenStreetMap contributors © CARTO', - url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', - markerStyle: "light", - default: mode == "light" - }, - { - name: "Stadia AlidadeSmoothDark", - attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', - url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png', - markerStyle: "dark", - default: mode == "dark" - }, - { - name: "Stadia AlidadeSatelite", - attribution: '© CNES, Distribution Airbus DS, © Airbus DS, © PlanetObserver (Contains Copernicus Data) | © Stadia Maps © OpenMapTiles © OpenStreetMap contributors', - url: 'https://tiles.stadiamaps.com/tiles/alidade_satellite/{z}/{x}/{y}{r}.jpg', - markerStyle: "dark" - }, - { - name: "Mapbox Satelite Streets", - attribution: '© Mapbox', - url: 'https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoidHlwZS1zdHlsZSIsImEiOiJjbGJ4aG14enEwZ2toM3BvNW5uanhuOGRvIn0.7TUEM9vA-EYSt3WW_bcsAA', - markerStyle: "dark" - } - ] - - // custom hook to handle map events and track active layer // used to switch marker design const LayerChangeHandler = () => { useMapEvents({ baselayerchange: (event) => { const newLayer = layers.filter((layer) => layer.name == event.name); - console.log(newLayer); if (newLayer[0].markerStyle != mapStyle) { setMapStyle(newLayer[0].markerStyle); } @@ -172,19 +116,22 @@ function Map({ entries }: { entries: Models.IEntry[] }) { return (
- + {layers.map((layer, index) => { return ( ) diff --git a/src/client/components/MiniMap.tsx b/src/client/components/MiniMap.tsx new file mode 100644 index 0000000..ddac239 --- /dev/null +++ b/src/client/components/MiniMap.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { MapContainer, TileLayer } from "react-leaflet"; +import { MapRecenter } from "./MapCenter"; + +export default function MiniMap({ layer, lastEntry, index }: client.MiniMapProps) { + function handleClick() { + const elements = document.querySelectorAll('input.leaflet-control-layers-selector'); + const el = elements[index] as HTMLInputElement | null; + if (!elements || !el) { return; } + + el.click(); + } + + return ( +
+ + + + +
); +} diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index fe6a957..fcef444 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -8,6 +8,9 @@ import ModeSwitcher from '../components/ModeSwitcher'; import Map from '../components/Map'; import Status from '../components/Status'; import LinearBuffer from "../components/LinearBuffer"; +import MiniMap from "../components/MiniMap"; +import { layers } from "../scripts/layers"; + function timeAgo(timestamp: number): string { if (!Number.isInteger(timestamp)) { @@ -150,9 +153,11 @@ function Start() {
-
image1
-
image2
-
image3
+ {entries.at(-1) && layers.map((layer, index) => { + return ( + + ) + })}
diff --git a/src/client/scripts/layers.ts b/src/client/scripts/layers.ts new file mode 100644 index 0000000..0ede698 --- /dev/null +++ b/src/client/scripts/layers.ts @@ -0,0 +1,48 @@ +export const layers:client.Layer[] = [ + { + name: "OSM DE", + attribution: '© OpenStreetMap contributors', + url: 'https://{s}.tile.openstreetmap.de/{z}/{x}/{y}.png', + markerStyle: "" + }, + { + name: "Carto Voyager Light", + attribution: '© OpenStreetMap contributors © CARTO', + url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', + markerStyle: "light", + default: "light" + }, + { + name: "Stadia AlidadeSmoothDark", + attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', + url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png', + markerStyle: "dark", + default: "dark" + }, + { + name: "ArcGis WorldImagery", + attribution: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', + url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', + markerStyle: "dark" + }, + // { + // name: "OpenRailway", + // attribution: 'Map data: © OpenStreetMap contributors | Map style: © Stadia Maps © OpenMapTiles © OpenStreetMap contributors', + url: 'https://tiles.stadiamaps.com/tiles/alidade_satellite/{z}/{x}/{y}{r}.jpg', + markerStyle: "dark" + }, + { + name: "Mapbox Satelite Streets", + attribution: '© Mapbox', + url: 'https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoidHlwZS1zdHlsZSIsImEiOiJjbGJ4aG14enEwZ2toM3BvNW5uanhuOGRvIn0.7TUEM9vA-EYSt3WW_bcsAA', + markerStyle: "dark", + size: 512, + zoomOffset: -1 + } +] \ No newline at end of file diff --git a/src/client/types.d.ts b/src/client/types.d.ts index 6b48c70..6542881 100644 --- a/src/client/types.d.ts +++ b/src/client/types.d.ts @@ -1,7 +1,23 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ declare module "*.module.css"; declare namespace client { + type MarkerStyle = "" | "light" | "dark"; + interface Layer { + name: string; + attribution: string; + url: string; + markerStyle: MarkerStyle; + default?: MarkerStyle; // Optional property since not all layers have a default style + size?: number; + zoomOffset?: number; + } + + interface MiniMapProps { + layer: client.Layer; + lastEntry: Models.IEntry + index?: number + } } From 494aa06d9a93a65e416a54aecb92187277904fd5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 17:28:41 +0200 Subject: [PATCH 185/206] Bump webpack from 5.91.0 to 5.94.0 (#117) Bumps [webpack](https://github.com/webpack/webpack) from 5.91.0 to 5.94.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.91.0...v5.94.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 47 +++++++++++++---------------------------------- package.json | 2 +- 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87064db..740c503 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,7 +72,7 @@ "ts-loader": "^9.5.1", "ts-node": "^10.9.2", "typescript": "^5.3.3", - "webpack": "^5.91.0", + "webpack": "^5.94.0", "webpack-cli": "^5.1.4" } }, @@ -2113,26 +2113,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "8.56.7", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz", - "integrity": "sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2927,10 +2907,10 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "peerDependencies": { "acorn": "^8" @@ -4432,9 +4412,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -13058,21 +13038,20 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", + "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", diff --git a/package.json b/package.json index 18f368e..83797bd 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "ts-loader": "^9.5.1", "ts-node": "^10.9.2", "typescript": "^5.3.3", - "webpack": "^5.91.0", + "webpack": "^5.94.0", "webpack-cli": "^5.1.4" }, "dependencies": { From 3ba611cf07da25a110ea9e7846b04ec734ef6e65 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 21:11:45 +0200 Subject: [PATCH 186/206] [Task] #83, code cleanup --- src/client/components/Map.tsx | 2 +- views/login-form.ejs | 31 ------------------------------- 2 files changed, 1 insertion(+), 32 deletions(-) delete mode 100644 views/login-form.ejs diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index 9882e0f..6d40d42 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -1,6 +1,6 @@ import React, { useContext, useState } from 'react' import { Context } from "../components/App"; -import { LayersControl, MapContainer, Marker, Polyline, Popup, TileLayer, useMap, useMapEvents } from 'react-leaflet' +import { LayersControl, MapContainer, Marker, Polyline, Popup, TileLayer, useMapEvents } from 'react-leaflet' import { toGamut, parse, Oklch, formatCss } from 'culori'; import L from 'leaflet'; import 'leaflet-rotatedmarker'; diff --git a/views/login-form.ejs b/views/login-form.ejs deleted file mode 100644 index b0eef5e..0000000 --- a/views/login-form.ejs +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - Login Form - Lorex - - - - - - - - - - \ No newline at end of file From ff36df763485e8c48bbc30bb68cbb4072fe0ffe5 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 21:31:04 +0200 Subject: [PATCH 187/206] [Fix] #83, mime type setting --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 92bc792..0759304 100644 --- a/src/app.ts +++ b/src/app.ts @@ -66,7 +66,7 @@ app.use('/login', loginRouter); // use httpdocs as static folder app.use('/', express.static(path.join(__dirname, 'httpdocs'), { - extensions: ['html', 'txt', "pdf"], + extensions: ['html', 'txt', "pdf", "css", "js"], index: ["start.html", "start.txt"], })); From d51e1ff77f738ad6da2c052128d9668b494474ae Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 1 Sep 2024 21:51:13 +0200 Subject: [PATCH 188/206] [fix] #123, run pre and post build for production too --- .github/workflows/ftp.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ftp.yml b/.github/workflows/ftp.yml index 653f6fb..3b123cf 100644 --- a/.github/workflows/ftp.yml +++ b/.github/workflows/ftp.yml @@ -15,8 +15,12 @@ jobs: node-version: 20 - name: Install dependencies run: npm i + - name: preBuild + run: npm run prebuild - name: Build run: npm run build:prod + - name: postBuild + run: npm run postbuild - name: copy views to be deployed run: | cp -R views/ dist/ From 5854f6f057481764eb7817e106ef3fd05db73d35 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 2 Sep 2024 10:23:43 +0200 Subject: [PATCH 189/206] [Change] #123, to see real user ip instead of localhost proxy (#126) --- src/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app.ts b/src/app.ts index 0759304..20bc2df 100644 --- a/src/app.ts +++ b/src/app.ts @@ -53,6 +53,7 @@ app.use((req, res, next) => { // limit body for specific http methods next(); }); +app.set('trust proxy',true); // routes app.get(['/', '/login'], (req, res) => { From e17dfb25da952fda0f7bc50de824194a943195c0 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 2 Sep 2024 10:40:15 +0200 Subject: [PATCH 190/206] Main (#128) * Release 1_3 (#124) * [Task] #6 provide fallback index.html * [Task] #6 production ready code (m) move httpdocs folder to dist have compile without sourcemaps for faster speed * [Task] #6 create github action for upload when main is updated (#21) * [change] #6 new ftp upload action * [Fix] #6 replace host with server in ftp action * [Task] #6 basic log (#26) * [CHANGE] #6 revert back to require output for production * [Task] #6 add ability to manually upload to prod * [Task] #9 enable manual start of codechecks * 10 webhook for writing (#36) * [Change] #3 clean up npm scripts, to have clean folder before build * [Task] #10 created data types in typescript * [Temp] #10 created subroute for writing, and folder structure * [Change] #3 include to use relative paths from src folder in ts and node https://stackoverflow.com/questions/43281741/how-can-i-use-paths-in-tsconfig-json See comment from Remo H. Hansen with at least 100 upvoted * [Change] Update VSCode to keep files open * [Task] #18 setup dotenv for secret variables * [Temp, Task] #10 Validate inputs using express-validator and custom functions * [Task] #18 prevent parameter pollution * [Task] #10 validating incoming parameter and logging errors * [Task] #7 add basic cache to express * [Changes] #7 Error Handling, to include basic custom Error Handling * [Task] #10 enhanced validation to only allow known parameters * [Change] #35 added Jest, tests for helper functions when writing * [Task] #10 better error Handling * [Task] #35 add tests for writing webhook validation * [TASK] #18 protect Webhook using KEY * [Fix] #35 test know import path structure now * [Task] #35 add test for protected webhook * [Task] #35 refactor build to run jest tests * [Task] #10 switched to crypto instead of bcrypt for dependency issue see synk inflight * [Fix] #36 PRQ Feedback * [Task] #3 improve error handling, logger and added chalk to colorize console output. Had to use chalk version 4 because of typescript converting to require, and chalk5 do want import syntax. * [Change] #3 nodemon to clear console when in dev mode * [!Task] #32 webhook creates folder and file based on date * [Change] #35 relocated tests and refactor write, also added file check * [Task] #18, installed helmet, configured self as CSP origin * [Fix] moved chalk out of dev dependency * [Task] #32 error logging and text output improvement, log string instead of "object" * [Task] #18 CSP Update to allow localhost for testing * [Fix] #3 debugging setup improvments * [FIX] #10 Error Handling * [Task] #10 writing basic non calculated data to file * [Fix] #10 avoid Header Modification after sending the request * [Task] #10 JSON Data pretty output * [Task] #32 update types to reflect subobjects of entry * [Task] #10 write time * [Task] #32 added logging for time edgecases * [Task] #10 output seconds * [Task] #10 calculate distance based on lat and lon * [Task] #32 writing tests for time and distance * [Task] #32 change distance calculation to use pythagoras * [Task] #38 add favicon * [Task] #32 time converted to seconds * [Taskk] #32 speed calculation and output and tests * [Task] #32 speed tests * [Task] #33 add ignore * [Task] #32 test finetuning * [Task] #32 add angle between entries * [Task] #32 test for angle, extracted getData function * [change] #32 test to include optional leading 0 for days * [!!!Task] #18 add uncaughtExeption handler as last resort * [Task] #7 enhance static options to include common filetypes; index file start is used as index file to avoid collisions with host provider * [change] #32 validation to be used more explictly * [change] #32 add index to log while writing * [Task] #32 test if 1000 calls can be made with randomized data * [!!! Task] #32 limit JSON Data to be 1000 lines: replace last line with most recent entry * [Change, Task] #32 if 1000 entries exceeded, only replace last if hdop is good * [Change] build action enable button to on manually * [temp] test y tests fail * Create node.js.yml * Create main.yml * [!!!Fix] Created new workflow to build / test node, commented tests back in. Increased time between server calls in test, to check difference time more accurately * [Task] #33 moved ignore to its own file since it creates data rather than validating it * 42 output json (#44) * [Task] #42, created route to output json * [Task] #42 added tests for read json * 41 add rate limiter (#45) * [Task] #18, limit request size for security reasons * [Task] #43, introduce gzip to transfer data * [Task] #34 improve error handling, log server shutdowns * [Task] #34 installed and integrated tooBusy to send 503 when load is high * [Task] #34 improved tooBusy, improved formatting * [Task, Temp] #41 installed ratelimiter and slowDown * [Task] #42 cleanup ipv6 addresses * [Change] #10 error handling for better gitBash and txt output, also reduced stack in case of validation errors * [Task] #41 prepare Log for RateLImit errors * [Temp] #41 write route rateLImited temp: see Todos * [Task] #34 colorize prefix in console * [Task] #42 extract middlewares and move to folder * [Task] #41 ratelimiter cleaning up periodicly * [Task] #41 skip tests in rateLimiting * Bump follow-redirects from 1.15.5 to 1.15.6 (#47) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * 43 secure output route (#46) * [Task] #43 create color pallette via atmos * [Task] #43 create color pallette via atmos * [Task] #43 cleanup colors and svg * [Task] #41 remove test code * [CHANGE] #3 reconfigured nodemon to copy static files * [Task] #18 replaced getRawBody with builtIn express urlEncoded * [Temp, Task] #43 basic login page, not yet used as middleware * [Temp] #43, create and validate json web token * [Task] #43, add slowDown and RateLimit for failed login attempts * [Task] #43, ratelimit for login page * [Task] #43, add global ratelimiter * [fix] #7, improve error handeling for express errors * [Task] #43 rework body limitations to be checked only appropiate methods * [Task] #43 added check for data before using it * [Task] #43 check that body is ignored for GET in request * [Task] #43 login test * [Task] #43 create tests for login * [Task] #43 fine tune error handling * [Task] #43, finished login and jwt related tests * [Change] #34, no further need for test logging * [Task] #43, fine tune jwt, middleware process improved * [CHANGE] #43 created new esLint to have clientside js without ts * [Temp] #43 test to see new linter configuration * [Change] #43 switched to bcrypt for passwords * [Task] #43 read return json in all cases * [Task] #43 introduced color classes * [Task] #43, prq feedback * [Temp} #43 figuring out why tests dont run on github * [Task] #43 code cleanup * 48 move login to seperate controller (#49) * [Task] #43, add label to form * [Task] #48 login controller * 50 integrate csrf protection for login form (#53) * [Task] #50, create CSRF Validation for login form * [Task] #43, added icon to repository for later use * [Task] #50, cleanup cetntralized; rename token functions * [Task] #50, reduced token length and improved error handling * [Task] #50 csrf tests added to login * [Task] #50, added test case for csrf, repaired integration * fix: upgrade express-rate-limit from 7.1.5 to 7.2.0 (#52) Snyk has created this PR to upgrade express-rate-limit from 7.1.5 to 7.2.0. See this package in npm: https://www.npmjs.com/package/express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: package.json & package-lock.json to reduce vulnerabilities (#54) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-EXPRESS-6474509 Co-authored-by: snyk-bot * [Snyk] Upgrade express from 4.18.2 to 4.18.3 (#51) * fix: upgrade express from 4.18.2 to 4.18.3 Snyk has created this PR to upgrade express from 4.18.2 to 4.18.3. See this package in npm: https://www.npmjs.com/package/express See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr * 50 integrate csrf protection for login form (#53) * [Task] #50, create CSRF Validation for login form * [Task] #43, added icon to repository for later use * [Task] #50, cleanup cetntralized; rename token functions * [Task] #50, reduced token length and improved error handling * [Task] #50 csrf tests added to login * [Task] #50, added test case for csrf, repaired integration * fix: upgrade express-rate-limit from 7.1.5 to 7.2.0 (#52) Snyk has created this PR to upgrade express-rate-limit from 7.1.5 to 7.2.0. See this package in npm: https://www.npmjs.com/package/express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --------- Co-authored-by: snyk-bot * [Task] update dev after main merge * [Task] npm upgrade * 58 react setup (#59) * [Task] #58 install react via npm, incl. types and eslint plugins * [Task] #58, tsconfig for react folder * [Task] #58 esLint config * [Task] #58, webpack and react setup * [Task] #58, render welcome from express instead of static * [Task] #58 eslint scripts * [Task] #58, eslint react setup * [TASK] #58 integrate webpack in build and dev npm scripts * [Temp] Test csp * [FIX] Add views to be deployed to prod * [Task] disable csp for local development * [Task] #58 base css including colors, deleted color classes in favor of variables * [Task] #58 typescript setup for react * [Task] #58 webpack setup for react and typescript * [Task] #58 app setup react * [Temp] #58 conctact module css * [Task] #58 remove learning files * [Task] #61, create font * Revert "[Task] #58 remove learning files" This reverts commit b63bb97045a9443e11ca9d8658f1e7faecf96e3b. * [Task] #61, adjust for darkmode * [Task] #61 apply base style to login * [Task] #58, dev tesing rule to disable * [Task] #58, adjust styles for headline * [Task] #58, create Contacts wrapper Component * [Task] #58 apply wrapper component * [Task] #58 adjust contact component to expect object * [Task] #58, toggle state * [Task] #58 learn context api provider and consumer * [Task] #58 add delete via dispatch * [Task] #58, react-router, move contacts to new url * [Task] #58 fetch more contacts * fix: package.json & package-lock.json to reduce vulnerabilities (#62) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-EJS-6689533 Co-authored-by: snyk-bot * Bump tar and npm (#60) Bumps [tar](https://github.com/isaacs/node-tar) to 6.2.1 and updates ancestor dependency [npm](https://github.com/npm/cli). These dependencies need to be updated together. Updates `tar` from 6.2.0 to 6.2.1 - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1) Updates `npm` from 10.5.0 to 10.5.2 - [Release notes](https://github.com/npm/cli/releases) - [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md) - [Commits](https://github.com/npm/cli/compare/v10.5.0...v10.5.2) --- updated-dependencies: - dependency-name: tar dependency-type: indirect - dependency-name: npm dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump ejs from 3.1.9 to 3.1.10 (#63) Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10. - [Release notes](https://github.com/mde/ejs/releases) - [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10) --- updated-dependencies: - dependency-name: ejs dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [Task] #58, webpack configuriation to allow regular css files as well as modules * [Task] #58, clean up react learn files * [Task] #58, setup react router * [Task] #61 install Material UI * [Task] #61, test mui * [CHANGE, MultiLine] #61 color variables levels removed, MUI Overwrites introduced color variables levels are replaced by color-mix. MUI Experimental API with Variables is used and to overwrite theme colors in css (since I want CSS to be single source of truth for colors) * [Temp] #61 introduce darkmode to MUI * [Task] #61, create new start module so that App can act as root * [Task] #61, naming update * [Task] #61, move router to root App * [Task] #61, add font to preload * [Task] #61, dim colors in dark mode * [Task] #61, introduce modeswitcher * [Change] #64, refactoring splitting pages and components * [Task] #61, mobile Theme Swticher placed on top right * [Task] #61, mobile theme switcher icon only on mobile * [Task] #61, button color and background improvement * [Task] #63, login page first draft * [Temp] #61, login controller commented out unused route, TO BE REFACTORED * [Task] #63, login validation * [Task] #63, add error icon * [Task] #61, cut design update * [Task] #53, apply cut, rename FormData to FormInfo to avoid confusion with reserved name * [Task] #63, send login request * [Task] #61, loading icon * [Task] #63, get csrfToken, fullfill login request * [Fix] #63, fail gracefully when too many tokens * [Task] #63, error Handling in login form * [Task] #81, remove password log * [Task] #80, cleanup todo token * fix: upgrade multiple dependencies with Snyk (#68) Snyk has created this PR to upgrade: - react from 18.2.0 to 18.3.1. See this package in npm: https://www.npmjs.com/package/react - react-dom from 18.2.0 to 18.3.1. See this package in npm: https://www.npmjs.com/package/react-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * [Fix] #64, disable express header * [Task] #64, protect csrf token page with custom http header * [FIx] #64, fix csrf test * [Task] #64, repair test cases * fix: upgrade express-slow-down from 2.0.1 to 2.0.2 (#69) Snyk has created this PR to upgrade express-slow-down from 2.0.1 to 2.0.2. See this package in npm: express-slow-down See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * [fix] #64, linter fixes * [Task] Editor Config * [Task] #61, convert background line to svg and animate * [Task] #61, main headline style * [Task] #61, fine tune background pattern * [Task] #61, font-weight reduced in darkmode * [Task] #64, login design improvements * [Task] #61, update design with minor ripples and edges * [Task] #70, store token after login * Bump braces from 3.0.2 to 3.0.3 (#76) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: upgrade @mui/icons-material from 5.15.16 to 5.15.18 (#75) Snyk has created this PR to upgrade @mui/icons-material from 5.15.16 to 5.15.18. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade react-router-dom from 6.23.0 to 6.23.1 (#74) Snyk has created this PR to upgrade react-router-dom from 6.23.0 to 6.23.1. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade express-slow-down from 2.0.2 to 2.0.3 (#73) Snyk has created this PR to upgrade express-slow-down from 2.0.2 to 2.0.3. See this package in npm: express-slow-down See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade express-rate-limit from 7.2.0 to 7.3.0 (#82) Snyk has created this PR to upgrade express-rate-limit from 7.2.0 to 7.3.0. See this package in npm: express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade express-validator from 7.0.1 to 7.1.0 (#81) Snyk has created this PR to upgrade express-validator from 7.0.1 to 7.1.0. See this package in npm: express-validator See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade axios from 1.7.1 to 1.7.2 (#80) Snyk has created this PR to upgrade axios from 1.7.1 to 1.7.2. See this package in npm: axios See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade @mui/icons-material from 5.15.18 to 5.15.19 (#79) Snyk has created this PR to upgrade @mui/icons-material from 5.15.18 to 5.15.19. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade @mui/icons-material from 5.15.19 to 5.15.20 (#88) Snyk has created this PR to upgrade @mui/icons-material from 5.15.19 to 5.15.20. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade react-router-dom from 6.23.1 to 6.24.0 (#91) Snyk has created this PR to upgrade react-router-dom from 6.23.1 to 6.24.0. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * 77 design base layout (#85) * [Task] #77 1st draft layout * [Change] #70 update token expire date * [Temp] #77, log out data on valid request, temp: error handling and display * [Temp] * [Task] #77, login Button functionality, default state * [Task] #77, removed outdated comments * [Task] #77, introduced linearBuffer Bar for login * [Task] #77, added modeSwticher to start page * [Task] #77, display last entry on map demo * [Task] #77, enhance login, show pastUser if availabe, show user on mainpage * [!!!Task] #77 first draft of functionality * [Task] #77 move map to new location * [Task] #77 create testData * [Fix] #77 codeFactor complains * [Task] #77, draft of status content * [FIX] #77 change data accumulation * [Task] #77 improve test example data * fix: upgrade @mui/material from 5.15.16 to 5.15.20 (#92) Snyk has created this PR to upgrade @mui/material from 5.15.16 to 5.15.20. See this package in npm: @mui/material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * [Fix] #94, refactor-ignore logic (multiline) (#95) Serverside: When writing entry, the most recent previous entry is checked wether to be ignored. Also if more than 2 items already exist meaning writing is preparing at least the 3rd entry, we recalculate distances and timing if previousItems are ignored. Frontend: In order to benefit and get the recent information that a previous item is being ignored, frontEnd askes for the current item again and merges it and following items. Remember the most recent item can never be ignored due to policy. Maybe there is no further writing, so I want to have the latest datapoint. * [Task] #94, add logging if logical error with ignore * [Task] #94, cleanup console.logs * [Fix] #93, offline message improvement (#96) * fix: package.json & package-lock.json to reduce vulnerabilities (#101) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-AXIOS-7361793 Co-authored-by: snyk-bot * fix: upgrade @mui/material from 5.15.20 to 5.16.5 (#102) Snyk has created this PR to upgrade @mui/material from 5.15.20 to 5.16.5. See this package in npm: @mui/material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * 93 fix error message when server not available (#103) * [Fix] #93, offline message improvement * [Task] #93, removed background in status module when no data is present * [Task] #61, add cut class to map for styling * [Fix] #93 fix tests, be more specific on url, and let test fail non silently when csrf is not found * [Fix] #94, repair overwriting the last data point * fix: upgrade @emotion/react from 11.11.4 to 11.13.0 (#104) Snyk has created this PR to upgrade @emotion/react from 11.11.4 to 11.13.0. See this package in npm: @emotion/react See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade @emotion/styled from 11.11.5 to 11.13.0 (#105) Snyk has created this PR to upgrade @emotion/styled from 11.11.5 to 11.13.0. See this package in npm: @emotion/styled See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade react-router-dom from 6.25.0 to 6.25.1 (#106) Snyk has created this PR to upgrade react-router-dom from 6.25.0 to 6.25.1. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade @mui/icons-material from 5.16.4 to 5.16.5 (#107) Snyk has created this PR to upgrade @mui/icons-material from 5.16.4 to 5.16.5. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade express-rate-limit from 7.3.1 to 7.4.0 (#108) Snyk has created this PR to upgrade express-rate-limit from 7.3.1 to 7.4.0. See this package in npm: express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * 109 marker and line design (#110) * [Task] #109, start polyline * [Task] #94, marker * [Task] #109, gradient color polyline color based on speed * [Task] #109 linter fixes * fix: upgrade react-router-dom from 6.25.1 to 6.26.0 (#113) Snyk has created this PR to upgrade react-router-dom from 6.25.1 to 6.26.0. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade @mui/material from 5.16.5 to 5.16.6 (#112) Snyk has created this PR to upgrade @mui/material from 5.16.5 to 5.16.6. See this package in npm: @mui/material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * Switch polyline (#114) * [Revert] #109 remove polyColor Plugin * [Fix, MultiLine] #109, refactor coloring lines; while fetchinng new data vs reloading MaxSpeed might change the more entries are fetched. Example Testcase and after 6 entries are there, reload and see colors change ... well not with this fix. * [Task] #109, improve polyline display, remove unused code * [Task] #77, change timing to round up, so it "feels" more accurate * [Task] #115, remove SVG Animation on startup based on media Query * [Task] #77 improve test example data * [Task] #77, calculate more Status data * [Task] #77, improve example test data * [Task] #109, line dashed when diff is high, marker start icon when diff is higher * [Task] #77, status design * [Task] #83, forced scheme for map * [Task] #83, change context, mode globally available * [Task] #83, react update, cluster install * [Task, multiline] #83, map tilelayer Introduced new map TileLayers using layers control, and styled it Introduced markerClusterGroup from @changey, since others had issues like broken marker images, or lack for typescript support Refined dashed array styles Addapted context changes, to fetch mode globally, for seperate map theme Markers have none style if neither end or start, to be targeted Introduced Layer array for tilelayers When Layers are changed theme for map is set/updated * [Task] #77 improve responsive design, hide images, status overlays map * [Task] #77, adjust coloring and opacity, used for status * [Task] #83, adjust imports * [Task] #77, changed responsive design for mobile * [Task] #83, extracted to own module, added fly option * [Task] #83, layers extracted, clickable minimap, corrected for mapbox tileSize * Bump webpack from 5.91.0 to 5.94.0 (#117) Bumps [webpack](https://github.com/webpack/webpack) from 5.91.0 to 5.94.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.91.0...v5.94.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [Task] #83, code cleanup * [Fix] #83, mime type setting * [fix] #123, run pre and post build for production too --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: snyk-bot * Release 1_4 (#127) * [Task] #6 provide fallback index.html * [Task] #6 production ready code (m) move httpdocs folder to dist have compile without sourcemaps for faster speed * [Task] #6 create github action for upload when main is updated (#21) * [change] #6 new ftp upload action * [Fix] #6 replace host with server in ftp action * [Task] #6 basic log (#26) * [CHANGE] #6 revert back to require output for production * [Task] #6 add ability to manually upload to prod * [Task] #9 enable manual start of codechecks * 10 webhook for writing (#36) * [Change] #3 clean up npm scripts, to have clean folder before build * [Task] #10 created data types in typescript * [Temp] #10 created subroute for writing, and folder structure * [Change] #3 include to use relative paths from src folder in ts and node https://stackoverflow.com/questions/43281741/how-can-i-use-paths-in-tsconfig-json See comment from Remo H. Hansen with at least 100 upvoted * [Change] Update VSCode to keep files open * [Task] #18 setup dotenv for secret variables * [Temp, Task] #10 Validate inputs using express-validator and custom functions * [Task] #18 prevent parameter pollution * [Task] #10 validating incoming parameter and logging errors * [Task] #7 add basic cache to express * [Changes] #7 Error Handling, to include basic custom Error Handling * [Task] #10 enhanced validation to only allow known parameters * [Change] #35 added Jest, tests for helper functions when writing * [Task] #10 better error Handling * [Task] #35 add tests for writing webhook validation * [TASK] #18 protect Webhook using KEY * [Fix] #35 test know import path structure now * [Task] #35 add test for protected webhook * [Task] #35 refactor build to run jest tests * [Task] #10 switched to crypto instead of bcrypt for dependency issue see synk inflight * [Fix] #36 PRQ Feedback * [Task] #3 improve error handling, logger and added chalk to colorize console output. Had to use chalk version 4 because of typescript converting to require, and chalk5 do want import syntax. * [Change] #3 nodemon to clear console when in dev mode * [!Task] #32 webhook creates folder and file based on date * [Change] #35 relocated tests and refactor write, also added file check * [Task] #18, installed helmet, configured self as CSP origin * [Fix] moved chalk out of dev dependency * [Task] #32 error logging and text output improvement, log string instead of "object" * [Task] #18 CSP Update to allow localhost for testing * [Fix] #3 debugging setup improvments * [FIX] #10 Error Handling * [Task] #10 writing basic non calculated data to file * [Fix] #10 avoid Header Modification after sending the request * [Task] #10 JSON Data pretty output * [Task] #32 update types to reflect subobjects of entry * [Task] #10 write time * [Task] #32 added logging for time edgecases * [Task] #10 output seconds * [Task] #10 calculate distance based on lat and lon * [Task] #32 writing tests for time and distance * [Task] #32 change distance calculation to use pythagoras * [Task] #38 add favicon * [Task] #32 time converted to seconds * [Taskk] #32 speed calculation and output and tests * [Task] #32 speed tests * [Task] #33 add ignore * [Task] #32 test finetuning * [Task] #32 add angle between entries * [Task] #32 test for angle, extracted getData function * [change] #32 test to include optional leading 0 for days * [!!!Task] #18 add uncaughtExeption handler as last resort * [Task] #7 enhance static options to include common filetypes; index file start is used as index file to avoid collisions with host provider * [change] #32 validation to be used more explictly * [change] #32 add index to log while writing * [Task] #32 test if 1000 calls can be made with randomized data * [!!! Task] #32 limit JSON Data to be 1000 lines: replace last line with most recent entry * [Change, Task] #32 if 1000 entries exceeded, only replace last if hdop is good * [Change] build action enable button to on manually * [temp] test y tests fail * Create node.js.yml * Create main.yml * [!!!Fix] Created new workflow to build / test node, commented tests back in. Increased time between server calls in test, to check difference time more accurately * [Task] #33 moved ignore to its own file since it creates data rather than validating it * 42 output json (#44) * [Task] #42, created route to output json * [Task] #42 added tests for read json * 41 add rate limiter (#45) * [Task] #18, limit request size for security reasons * [Task] #43, introduce gzip to transfer data * [Task] #34 improve error handling, log server shutdowns * [Task] #34 installed and integrated tooBusy to send 503 when load is high * [Task] #34 improved tooBusy, improved formatting * [Task, Temp] #41 installed ratelimiter and slowDown * [Task] #42 cleanup ipv6 addresses * [Change] #10 error handling for better gitBash and txt output, also reduced stack in case of validation errors * [Task] #41 prepare Log for RateLImit errors * [Temp] #41 write route rateLImited temp: see Todos * [Task] #34 colorize prefix in console * [Task] #42 extract middlewares and move to folder * [Task] #41 ratelimiter cleaning up periodicly * [Task] #41 skip tests in rateLimiting * Bump follow-redirects from 1.15.5 to 1.15.6 (#47) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * 43 secure output route (#46) * [Task] #43 create color pallette via atmos * [Task] #43 create color pallette via atmos * [Task] #43 cleanup colors and svg * [Task] #41 remove test code * [CHANGE] #3 reconfigured nodemon to copy static files * [Task] #18 replaced getRawBody with builtIn express urlEncoded * [Temp, Task] #43 basic login page, not yet used as middleware * [Temp] #43, create and validate json web token * [Task] #43, add slowDown and RateLimit for failed login attempts * [Task] #43, ratelimit for login page * [Task] #43, add global ratelimiter * [fix] #7, improve error handeling for express errors * [Task] #43 rework body limitations to be checked only appropiate methods * [Task] #43 added check for data before using it * [Task] #43 check that body is ignored for GET in request * [Task] #43 login test * [Task] #43 create tests for login * [Task] #43 fine tune error handling * [Task] #43, finished login and jwt related tests * [Change] #34, no further need for test logging * [Task] #43, fine tune jwt, middleware process improved * [CHANGE] #43 created new esLint to have clientside js without ts * [Temp] #43 test to see new linter configuration * [Change] #43 switched to bcrypt for passwords * [Task] #43 read return json in all cases * [Task] #43 introduced color classes * [Task] #43, prq feedback * [Temp} #43 figuring out why tests dont run on github * [Task] #43 code cleanup * 48 move login to seperate controller (#49) * [Task] #43, add label to form * [Task] #48 login controller * 50 integrate csrf protection for login form (#53) * [Task] #50, create CSRF Validation for login form * [Task] #43, added icon to repository for later use * [Task] #50, cleanup cetntralized; rename token functions * [Task] #50, reduced token length and improved error handling * [Task] #50 csrf tests added to login * [Task] #50, added test case for csrf, repaired integration * fix: upgrade express-rate-limit from 7.1.5 to 7.2.0 (#52) Snyk has created this PR to upgrade express-rate-limit from 7.1.5 to 7.2.0. See this package in npm: https://www.npmjs.com/package/express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: package.json & package-lock.json to reduce vulnerabilities (#54) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-EXPRESS-6474509 Co-authored-by: snyk-bot * [Snyk] Upgrade express from 4.18.2 to 4.18.3 (#51) * fix: upgrade express from 4.18.2 to 4.18.3 Snyk has created this PR to upgrade express from 4.18.2 to 4.18.3. See this package in npm: https://www.npmjs.com/package/express See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr * 50 integrate csrf protection for login form (#53) * [Task] #50, create CSRF Validation for login form * [Task] #43, added icon to repository for later use * [Task] #50, cleanup cetntralized; rename token functions * [Task] #50, reduced token length and improved error handling * [Task] #50 csrf tests added to login * [Task] #50, added test case for csrf, repaired integration * fix: upgrade express-rate-limit from 7.1.5 to 7.2.0 (#52) Snyk has created this PR to upgrade express-rate-limit from 7.1.5 to 7.2.0. See this package in npm: https://www.npmjs.com/package/express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --------- Co-authored-by: snyk-bot * [Task] update dev after main merge * [Task] npm upgrade * 58 react setup (#59) * [Task] #58 install react via npm, incl. types and eslint plugins * [Task] #58, tsconfig for react folder * [Task] #58 esLint config * [Task] #58, webpack and react setup * [Task] #58, render welcome from express instead of static * [Task] #58 eslint scripts * [Task] #58, eslint react setup * [TASK] #58 integrate webpack in build and dev npm scripts * [Temp] Test csp * [FIX] Add views to be deployed to prod * [Task] disable csp for local development * [Task] #58 base css including colors, deleted color classes in favor of variables * [Task] #58 typescript setup for react * [Task] #58 webpack setup for react and typescript * [Task] #58 app setup react * [Temp] #58 conctact module css * [Task] #58 remove learning files * [Task] #61, create font * Revert "[Task] #58 remove learning files" This reverts commit b63bb97045a9443e11ca9d8658f1e7faecf96e3b. * [Task] #61, adjust for darkmode * [Task] #61 apply base style to login * [Task] #58, dev tesing rule to disable * [Task] #58, adjust styles for headline * [Task] #58, create Contacts wrapper Component * [Task] #58 apply wrapper component * [Task] #58 adjust contact component to expect object * [Task] #58, toggle state * [Task] #58 learn context api provider and consumer * [Task] #58 add delete via dispatch * [Task] #58, react-router, move contacts to new url * [Task] #58 fetch more contacts * fix: package.json & package-lock.json to reduce vulnerabilities (#62) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-EJS-6689533 Co-authored-by: snyk-bot * Bump tar and npm (#60) Bumps [tar](https://github.com/isaacs/node-tar) to 6.2.1 and updates ancestor dependency [npm](https://github.com/npm/cli). These dependencies need to be updated together. Updates `tar` from 6.2.0 to 6.2.1 - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1) Updates `npm` from 10.5.0 to 10.5.2 - [Release notes](https://github.com/npm/cli/releases) - [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md) - [Commits](https://github.com/npm/cli/compare/v10.5.0...v10.5.2) --- updated-dependencies: - dependency-name: tar dependency-type: indirect - dependency-name: npm dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump ejs from 3.1.9 to 3.1.10 (#63) Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10. - [Release notes](https://github.com/mde/ejs/releases) - [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10) --- updated-dependencies: - dependency-name: ejs dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [Task] #58, webpack configuriation to allow regular css files as well as modules * [Task] #58, clean up react learn files * [Task] #58, setup react router * [Task] #61 install Material UI * [Task] #61, test mui * [CHANGE, MultiLine] #61 color variables levels removed, MUI Overwrites introduced color variables levels are replaced by color-mix. MUI Experimental API with Variables is used and to overwrite theme colors in css (since I want CSS to be single source of truth for colors) * [Temp] #61 introduce darkmode to MUI * [Task] #61, create new start module so that App can act as root * [Task] #61, naming update * [Task] #61, move router to root App * [Task] #61, add font to preload * [Task] #61, dim colors in dark mode * [Task] #61, introduce modeswitcher * [Change] #64, refactoring splitting pages and components * [Task] #61, mobile Theme Swticher placed on top right * [Task] #61, mobile theme switcher icon only on mobile * [Task] #61, button color and background improvement * [Task] #63, login page first draft * [Temp] #61, login controller commented out unused route, TO BE REFACTORED * [Task] #63, login validation * [Task] #63, add error icon * [Task] #61, cut design update * [Task] #53, apply cut, rename FormData to FormInfo to avoid confusion with reserved name * [Task] #63, send login request * [Task] #61, loading icon * [Task] #63, get csrfToken, fullfill login request * [Fix] #63, fail gracefully when too many tokens * [Task] #63, error Handling in login form * [Task] #81, remove password log * [Task] #80, cleanup todo token * fix: upgrade multiple dependencies with Snyk (#68) Snyk has created this PR to upgrade: - react from 18.2.0 to 18.3.1. See this package in npm: https://www.npmjs.com/package/react - react-dom from 18.2.0 to 18.3.1. See this package in npm: https://www.npmjs.com/package/react-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * [Fix] #64, disable express header * [Task] #64, protect csrf token page with custom http header * [FIx] #64, fix csrf test * [Task] #64, repair test cases * fix: upgrade express-slow-down from 2.0.1 to 2.0.2 (#69) Snyk has created this PR to upgrade express-slow-down from 2.0.1 to 2.0.2. See this package in npm: express-slow-down See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * [fix] #64, linter fixes * [Task] Editor Config * [Task] #61, convert background line to svg and animate * [Task] #61, main headline style * [Task] #61, fine tune background pattern * [Task] #61, font-weight reduced in darkmode * [Task] #64, login design improvements * [Task] #61, update design with minor ripples and edges * [Task] #70, store token after login * Bump braces from 3.0.2 to 3.0.3 (#76) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: upgrade @mui/icons-material from 5.15.16 to 5.15.18 (#75) Snyk has created this PR to upgrade @mui/icons-material from 5.15.16 to 5.15.18. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade react-router-dom from 6.23.0 to 6.23.1 (#74) Snyk has created this PR to upgrade react-router-dom from 6.23.0 to 6.23.1. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade express-slow-down from 2.0.2 to 2.0.3 (#73) Snyk has created this PR to upgrade express-slow-down from 2.0.2 to 2.0.3. See this package in npm: express-slow-down See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade express-rate-limit from 7.2.0 to 7.3.0 (#82) Snyk has created this PR to upgrade express-rate-limit from 7.2.0 to 7.3.0. See this package in npm: express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade express-validator from 7.0.1 to 7.1.0 (#81) Snyk has created this PR to upgrade express-validator from 7.0.1 to 7.1.0. See this package in npm: express-validator See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade axios from 1.7.1 to 1.7.2 (#80) Snyk has created this PR to upgrade axios from 1.7.1 to 1.7.2. See this package in npm: axios See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade @mui/icons-material from 5.15.18 to 5.15.19 (#79) Snyk has created this PR to upgrade @mui/icons-material from 5.15.18 to 5.15.19. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade @mui/icons-material from 5.15.19 to 5.15.20 (#88) Snyk has created this PR to upgrade @mui/icons-material from 5.15.19 to 5.15.20. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade react-router-dom from 6.23.1 to 6.24.0 (#91) Snyk has created this PR to upgrade react-router-dom from 6.23.1 to 6.24.0. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * 77 design base layout (#85) * [Task] #77 1st draft layout * [Change] #70 update token expire date * [Temp] #77, log out data on valid request, temp: error handling and display * [Temp] * [Task] #77, login Button functionality, default state * [Task] #77, removed outdated comments * [Task] #77, introduced linearBuffer Bar for login * [Task] #77, added modeSwticher to start page * [Task] #77, display last entry on map demo * [Task] #77, enhance login, show pastUser if availabe, show user on mainpage * [!!!Task] #77 first draft of functionality * [Task] #77 move map to new location * [Task] #77 create testData * [Fix] #77 codeFactor complains * [Task] #77, draft of status content * [FIX] #77 change data accumulation * [Task] #77 improve test example data * fix: upgrade @mui/material from 5.15.16 to 5.15.20 (#92) Snyk has created this PR to upgrade @mui/material from 5.15.16 to 5.15.20. See this package in npm: @mui/material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * [Fix] #94, refactor-ignore logic (multiline) (#95) Serverside: When writing entry, the most recent previous entry is checked wether to be ignored. Also if more than 2 items already exist meaning writing is preparing at least the 3rd entry, we recalculate distances and timing if previousItems are ignored. Frontend: In order to benefit and get the recent information that a previous item is being ignored, frontEnd askes for the current item again and merges it and following items. Remember the most recent item can never be ignored due to policy. Maybe there is no further writing, so I want to have the latest datapoint. * [Task] #94, add logging if logical error with ignore * [Task] #94, cleanup console.logs * [Fix] #93, offline message improvement (#96) * fix: package.json & package-lock.json to reduce vulnerabilities (#101) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-AXIOS-7361793 Co-authored-by: snyk-bot * fix: upgrade @mui/material from 5.15.20 to 5.16.5 (#102) Snyk has created this PR to upgrade @mui/material from 5.15.20 to 5.16.5. See this package in npm: @mui/material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * 93 fix error message when server not available (#103) * [Fix] #93, offline message improvement * [Task] #93, removed background in status module when no data is present * [Task] #61, add cut class to map for styling * [Fix] #93 fix tests, be more specific on url, and let test fail non silently when csrf is not found * [Fix] #94, repair overwriting the last data point * fix: upgrade @emotion/react from 11.11.4 to 11.13.0 (#104) Snyk has created this PR to upgrade @emotion/react from 11.11.4 to 11.13.0. See this package in npm: @emotion/react See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade @emotion/styled from 11.11.5 to 11.13.0 (#105) Snyk has created this PR to upgrade @emotion/styled from 11.11.5 to 11.13.0. See this package in npm: @emotion/styled See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade react-router-dom from 6.25.0 to 6.25.1 (#106) Snyk has created this PR to upgrade react-router-dom from 6.25.0 to 6.25.1. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade @mui/icons-material from 5.16.4 to 5.16.5 (#107) Snyk has created this PR to upgrade @mui/icons-material from 5.16.4 to 5.16.5. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade express-rate-limit from 7.3.1 to 7.4.0 (#108) Snyk has created this PR to upgrade express-rate-limit from 7.3.1 to 7.4.0. See this package in npm: express-rate-limit See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * 109 marker and line design (#110) * [Task] #109, start polyline * [Task] #94, marker * [Task] #109, gradient color polyline color based on speed * [Task] #109 linter fixes * fix: upgrade react-router-dom from 6.25.1 to 6.26.0 (#113) Snyk has created this PR to upgrade react-router-dom from 6.25.1 to 6.26.0. See this package in npm: react-router-dom See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade @mui/material from 5.16.5 to 5.16.6 (#112) Snyk has created this PR to upgrade @mui/material from 5.16.5 to 5.16.6. See this package in npm: @mui/material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * Switch polyline (#114) * [Revert] #109 remove polyColor Plugin * [Fix, MultiLine] #109, refactor coloring lines; while fetchinng new data vs reloading MaxSpeed might change the more entries are fetched. Example Testcase and after 6 entries are there, reload and see colors change ... well not with this fix. * [Task] #109, improve polyline display, remove unused code * [Task] #77, change timing to round up, so it "feels" more accurate * [Task] #115, remove SVG Animation on startup based on media Query * [Task] #77 improve test example data * [Task] #77, calculate more Status data * [Task] #77, improve example test data * [Task] #109, line dashed when diff is high, marker start icon when diff is higher * [Task] #77, status design * [Task] #83, forced scheme for map * [Task] #83, change context, mode globally available * [Task] #83, react update, cluster install * [Task, multiline] #83, map tilelayer Introduced new map TileLayers using layers control, and styled it Introduced markerClusterGroup from @changey, since others had issues like broken marker images, or lack for typescript support Refined dashed array styles Addapted context changes, to fetch mode globally, for seperate map theme Markers have none style if neither end or start, to be targeted Introduced Layer array for tilelayers When Layers are changed theme for map is set/updated * [Task] #77 improve responsive design, hide images, status overlays map * [Task] #77, adjust coloring and opacity, used for status * [Task] #83, adjust imports * [Task] #77, changed responsive design for mobile * [Task] #83, extracted to own module, added fly option * [Task] #83, layers extracted, clickable minimap, corrected for mapbox tileSize * Bump webpack from 5.91.0 to 5.94.0 (#117) Bumps [webpack](https://github.com/webpack/webpack) from 5.91.0 to 5.94.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.91.0...v5.94.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [Task] #83, code cleanup * [Fix] #83, mime type setting * [fix] #123, run pre and post build for production too * [Change] #123, to see real user ip instead of localhost proxy (#126) --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: snyk-bot --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: snyk-bot From e5fb3abe444dd55ee2a596cfb10581aeda0eaf42 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 2 Sep 2024 10:40:53 +0200 Subject: [PATCH 191/206] 125 show old data from yesterday (#129) * [Change] #123, to see real user ip instead of localhost proxy * [Task] #125, show old data * [fix] #77, update marker cluster design --- src/client/css/map.css | 2 +- src/controller/read.ts | 5 ++- src/models/entry.ts | 9 +++-- src/scripts/file.ts | 69 ++++++++++++++++++++++++++++------- src/tests/integration.test.ts | 2 +- types.d.ts | 4 +- 6 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/client/css/map.css b/src/client/css/map.css index b2646a7..264d09a 100644 --- a/src/client/css/map.css +++ b/src/client/css/map.css @@ -28,7 +28,7 @@ filter: drop-shadow(0px 0px 3px var(--neutral)); } -.marker-cluster-small[class] { /* overwrite default cluster style */ +.marker-cluster[class] { /* overwrite default cluster style */ background: none; div { background-color: var(--semiContrastBackground); diff --git a/src/controller/read.ts b/src/controller/read.ts index bd4b88c..4106251 100644 --- a/src/controller/read.ts +++ b/src/controller/read.ts @@ -17,7 +17,10 @@ router.get('/', return createError(res, 400, JSON.stringify({ errors: errors.array() }), next) } - const fileObj: File.Obj = file.getFile(res, next); + const fileObj: File.Obj = file.getFile(res, next, "read"); + // no content and no file found show empty data and exit + if (fileObj.content == false) { res.json(JSON.parse('{"entries": []}')); return } + fileObj.content = await file.readAsJson(res, fileObj.path, next) if (!fileObj.content || !Array.isArray(fileObj.content.entries)) { return createError(res, undefined, `File corrupt: ${fileObj.path}`, next); diff --git a/src/models/entry.ts b/src/models/entry.ts index 34dbb75..799177b 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -13,12 +13,13 @@ import logger from '@src/scripts/logger'; export const entry = { create: async (req: Request, res: Response, next: NextFunction) => { - const fileObj: File.Obj = file.getFile(res, next); + const fileObj: File.Obj = file.getFile(res, next, "write"); + if (!fileObj.content) { return createError(res, 500, "File does not exist: " + fileObj.path, next); } + fileObj.content = await file.readAsJson(res, fileObj.path, next); - if (!fileObj.content?.entries) { - return createError(res, 500, "File Content unavailable: " + fileObj.path, next); - } + if (!fileObj.content?.entries) {return createError(res, 500, "File Content unavailable: " + fileObj.path, next); } + const entries = fileObj.content.entries; const lastEntry = fileObj.content.entries.at(-1); let previousEntry = fileObj.content.entries.at(-1); // potentially overwritten if entry is set to ignore diff --git a/src/scripts/file.ts b/src/scripts/file.ts index a8b693b..fc8d013 100644 --- a/src/scripts/file.ts +++ b/src/scripts/file.ts @@ -5,11 +5,11 @@ import { create as createError } from '@src/middleware/error'; import { NextFunction, Response } from 'express'; import logger from '@src/scripts/logger'; -export const getFile = (res: Response, next: NextFunction): File.Obj => { +export const getFile = (res: Response, next: NextFunction, method: File.method): File.Obj => { const date = new Date(); const formattedDate = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; const dirPath = path.resolve(__dirname, '../data'); - const filePath = path.resolve(dirPath, `data-${formattedDate}.json`); + let filePath = path.resolve(dirPath, `data-${formattedDate}.json`); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); @@ -17,17 +17,24 @@ export const getFile = (res: Response, next: NextFunction): File.Obj => { } let fileExisted = true; - if (!fs.existsSync(filePath)) { // check if file exist + let olderFile = false; + if (!fs.existsSync(filePath)) { // if file does not exist fileExisted = false; - try { - fs.writeFileSync(filePath, '{"entries": []}'); - logger.log(`file: ${filePath} did not exist, but created now`); - } catch (err) { - createError(res, 500, "File cannot be written to", next); + const mostRecentFile = findMostRecentFile(dirPath); + if (method == "read" && mostRecentFile) { // when reading check old files + olderFile = true; + filePath = mostRecentFile; + } else if (method == "write") { + try { + fs.writeFileSync(filePath, '{"entries": []}'); + logger.log(`file: ${filePath} did not exist, but created now`); + } catch (err) { + createError(res, 500, "File cannot be written to", next); + } } } - return { path: filePath, content: fileExisted ? undefined : JSON.parse('{"entries": []}') }; // if the file did not exist before, the content is emptyString + return { path: filePath, content: method == "read" ? (fileExisted || olderFile) : JSON.parse('{"entries": []}') }; }; @@ -39,24 +46,58 @@ export async function readAsJson(res: Response, filePath: string, next: NextFunc try { return JSON.parse(data); } catch (err) { - createError(res, 500, "File contains wrong content: " + filePath, next); + createError(res, 500, "File contains wrong content: " + path.basename(filePath), next); } } -export const write = (res:Response, fileObj:File.Obj, next: NextFunction) => { +export const write = (res: Response, fileObj: File.Obj, next: NextFunction) => { if (!fs.existsSync(fileObj.path)) { // check if file exist createError(res, 500, "Can not write to file that does not exist: " + fileObj.path, next); } + + if (typeof fileObj.content == "boolean") { + createError(res, 500, `File (${fileObj.path}) cannot be written to, contents are not correct type`, next); + } + try { const content = JSON.stringify(fileObj.content, undefined, 2); fs.writeFileSync(fileObj.path, content); fileObj.content = JSON.parse(content); - logger.log(`written to file: ${fileObj.path} ${fileObj.content ? fileObj.content?.entries.length - 1 : ''}`); + if (typeof fileObj.content != "boolean") { + logger.log(`written to file: ${fileObj.path} ${fileObj.content?.entries ? fileObj.content.entries.length - 1 : ''}`); + } } catch (err) { createError(res, 500, `File (${fileObj.path}) cannot be written to`, next); - } + } + - return fileObj; // if the file did not exist before, the content is emptyString + return fileObj; }; + +const findMostRecentFile = (directoryPath: string) => { + // read all files from the directory + const files = fs.readdirSync(directoryPath); + + // initialize variables to keep track of the most recent file + let mostRecentFile = null; + let mostRecentTime = 0; + + files.forEach((file) => { + const filePath = path.join(directoryPath, file); + + // get file stats (including modified time) + const stats = fs.statSync(filePath); + + // check if it is a file (and not a directory) and most recent + if (stats.isFile() && stats.mtimeMs > mostRecentTime) { + if (stats.mtimeMs > mostRecentTime) { + mostRecentTime &&= stats.mtimeMs; + mostRecentFile = filePath; + } + } + }); + + return mostRecentFile; +} diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index e79be6f..9cad2b5 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -223,7 +223,7 @@ describe('API calls', () => { const response = await axios.get(url); expect(response.status).toBe(200); } - }, 20000); // adjust this to to fit your setup + }, 22000); // adjust this to to fit your setup test(`length of json should not exceed 1000`, async () => { const date = new Date(); diff --git a/types.d.ts b/types.d.ts index 0bab5de..f246bc4 100644 --- a/types.d.ts +++ b/types.d.ts @@ -24,8 +24,10 @@ namespace Response { namespace File { interface Obj { path: string, - content?: Models.IEntries; + content?: Models.IEntries | boolean; } + + type method = 'read' | 'write'; } namespace Models { From 1c9bfdc7cfeb0b00aed59b757697ad69f1239809 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 2 Sep 2024 21:20:25 +0200 Subject: [PATCH 192/206] fix: upgrade express-validator from 7.1.0 to 7.2.0 (#133) Snyk has created this PR to upgrade express-validator from 7.1.0 to 7.2.0. See this package in npm: express-validator See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 740c503..d6cac98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "express": "^4.19.2", "express-rate-limit": "^7.4.0", "express-slow-down": "^2.0.3", - "express-validator": "^7.1.0", + "express-validator": "^7.2.0", "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", @@ -5371,9 +5371,9 @@ } }, "node_modules/express-validator": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.1.0.tgz", - "integrity": "sha512-ePn6NXjHRZiZkwTiU1Rl2hy6aUqmi6Cb4/s8sfUsKH7j2yYl9azSpl8xEHcOj1grzzQ+UBEoLWtE1s6FDxW++g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.0.tgz", + "integrity": "sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA==", "license": "MIT", "dependencies": { "lodash": "^4.17.21", diff --git a/package.json b/package.json index 83797bd..265196a 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "express": "^4.19.2", "express-rate-limit": "^7.4.0", "express-slow-down": "^2.0.3", - "express-validator": "^7.1.0", + "express-validator": "^7.2.0", "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", From 4650423d755201bce00ce2bbb01e84859370e935 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 2 Sep 2024 21:20:37 +0200 Subject: [PATCH 193/206] fix: upgrade @mui/material from 5.16.6 to 5.16.7 (#132) Snyk has created this PR to upgrade @mui/material from 5.16.6 to 5.16.7. See this package in npm: @mui/material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index d6cac98..81d7a12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.5", - "@mui/material": "^5.16.6", + "@mui/material": "^5.16.7", "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.4", "bcrypt": "^5.1.1", @@ -1723,14 +1723,14 @@ } }, "node_modules/@mui/material": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.6.tgz", - "integrity": "sha512-0LUIKBOIjiFfzzFNxXZBRAyr9UQfmTAFzbt6ziOU2FDXhorNN2o3N9/32mNJbCA8zJo2FqFU6d3dtoqUDyIEfA==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", + "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.16.6", - "@mui/system": "^5.16.6", + "@mui/core-downloads-tracker": "^5.16.7", + "@mui/system": "^5.16.7", "@mui/types": "^7.2.15", "@mui/utils": "^5.16.6", "@popperjs/core": "^2.11.8", diff --git a/package.json b/package.json index 265196a..eb421a2 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.5", - "@mui/material": "^5.16.6", + "@mui/material": "^5.16.7", "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.4", "bcrypt": "^5.1.1", From 1d96e6dd299d7269003e1355ec3b30281709f5e6 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 3 Sep 2024 11:05:24 +0200 Subject: [PATCH 194/206] Create .env For easier setup, to follow readme installation guide --- .env | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..319d1c2 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +NODE_ENV=development +ROOT=https://your-produciton-server.com +KEY= +USER_JOHNDOE= +USER_TEST= +LOCALHOST=127.0.0.1 +LOCALHOSTv6=::1 From 7c9343bf7fb988c1f1bdf2f42049432de4fb4e3c Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 3 Sep 2024 11:49:13 +0200 Subject: [PATCH 195/206] Update README.md Installation Guide --- README.md | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 228b1b5..f415bf9 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,7 @@ Leaflet Osmand React ExpressJS Coordinates (X) > Remember an "X" marks the spot. ## Goal -**Technical:** Recieve and store coordinates via webhook and display them on a map through an interactive frontend. -**Personal:** The intend of this project is to get familiar with the listed technologies. - -## Progress -View [milestones](https://github.com/Type-Style/LOREX/milestones) and the [board](https://github.com/users/Type-Style/projects/1) to witness progression +Recieve and store coordinates via webhook and display them on a map through an interactive frontend. ## Installation ### Prerequisites @@ -20,6 +16,87 @@ Run the install command for example via npm `npm install` After completion without errors hit -`npx ts-node src/app.ts` +`npm run build` + +Once complete you can start the server using +`npm run start` + +> [!TIP] +Build and start can be combined using +`npm run dev` + +### Generating Key and Password +`Error Message: KEY is missing in environment variables` +Before you are able to login and use the webapplication, environment variables need to be setup. +Therefore you need to create a file `.env` and place it in the root of the project. +This file will be filled with secrets to protect your instance of LOREX. + +####1st. Generate Key +Usage: open console run: node init/generateKey.js +type desired key and hit enter +copy output to .env add a line starting with: +KEY= +directly followed by your output + +####2nd Generate Password(s) +Prerequisite: KEY already generated! +_(may require server restart)_ +Run the build command from the package.json (npm run build) +Then call the compiled version of this script using the key as environment variable like so: +KEY=your-key node ./init/generatePassword.js +Enter your password +Copy that to the Environment Variables and .env file +USER_WHATEVER= +followed by the output of the console + +> [!IMPORTANT] +In order to run automatic tests and create example data is is highly recommended to have a USER_TEST with the password of `test` +THe test user cannot be used in an production environment + +Once completed rebuild / restart the server and open up localhost/login +Login is now possible using the Username from the .env file in this example "WHATEVER" and the password that was created in the previous step. + +**Now the application is ready** + + +## Generating Data +### Example Data +Build and start the server for example using +`npm run dev` +Wait for the server to start and webpack to compile the assets. +Once done use a second command line to run +`npm run test:data` + +This generates 6 entries each 30s appart and calls the webserver to store this data. +The writing of data can be seen in the first console where the server is running. +Also once logged in under localhost, a map is visible showing a route westbound from the brandenburg gate. + +### Calling writing route manually +Data can be generated by calling the /write rout of the server. +Here is an example: + +`http://localhost/write?user=xx&lat=53.5000&lon=10.0×tamp=1720691648188&hdop=10.0&altitude=1000.000&speed=100.000&heading=180.0&key=test` + + +In order to pass validation use the correct key _(or `test` in development envrioment)_ and create a valid [UNIX timestamp]([url](https://currentmillis.com/)). +For example by using this javascript code: +`var a = new Date().getTime(); +copy(a); +a;` + +## Using on Production +### A note on security +This application is not developed with https built in support. +> [!WARNING] It is advised to run this application in `production` mode behind a proxy that uses https for security reasons + +### Getting data +Similar to the section Generating Data and Calling writing route manually, the application relies on data being provided using a webhook. +Well how data is collected and what data is pushed to the system is user preference. +Feel free to build you own application to do so. + +This application is designed to be used with the [OSMAND+ mobile app]([url](https://osmand.net/)). +Due to a plugin called [Triprecording]([url](https://osmand.net/docs/user/plugins/trip-recording/)) +Using the above link or by [clicking here](https://osmand.net/docs/user/plugins/trip-recording#recording-settings) more information can be found to setup webtracking or "online tracking" + -**Now the server is ready** + From 6405562a47194fa8da90d9df7146ec07b212a0a0 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 3 Sep 2024 11:49:41 +0200 Subject: [PATCH 196/206] fix: upgrade @mui/icons-material from 5.16.5 to 5.16.7 (#134) Snyk has created this PR to upgrade @mui/icons-material from 5.16.5 to 5.16.7. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 81d7a12..7843c4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@changey/react-leaflet-markercluster": "^4.0.0-rc1", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.16.5", + "@mui/icons-material": "^5.16.7", "@mui/material": "^5.16.7", "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.4", @@ -1697,9 +1697,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.5.tgz", - "integrity": "sha512-bn88xxU/J9UV0s6+eutq7o3TTOrOlbCX+KshFb8kxgIxJZZfYz3JbAXVMivvoMF4Md6jCVUzM9HEkf4Ajab4tw==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", + "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" diff --git a/package.json b/package.json index eb421a2..02d40a3 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@changey/react-leaflet-markercluster": "^4.0.0-rc1", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.16.5", + "@mui/icons-material": "^5.16.7", "@mui/material": "^5.16.7", "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.4", From 33f4227a46ac10f4691030224d0842a87e2259b8 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 3 Sep 2024 12:02:39 +0200 Subject: [PATCH 197/206] Rename .env to .env_example --- .env => .env_example | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .env => .env_example (100%) diff --git a/.env b/.env_example similarity index 100% rename from .env rename to .env_example From 6e4be920379d8edc7af62ccc2eeee4ea140dd1bf Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 3 Sep 2024 12:14:37 +0200 Subject: [PATCH 198/206] =?UTF-8?q?[CHANGE]=20#84,=20writing=20key=20check?= =?UTF-8?q?=20to=20be=20simpler,=20introduced=20scripts=20for=E2=80=A6=20(?= =?UTF-8?q?#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [CHANGE] #84, writing key check to be simpler, introduced scripts for setting up environment variables * fix: upgrade express-validator from 7.1.0 to 7.2.0 (#133) Snyk has created this PR to upgrade express-validator from 7.1.0 to 7.2.0. See this package in npm: express-validator See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * fix: upgrade @mui/material from 5.16.6 to 5.16.7 (#132) Snyk has created this PR to upgrade @mui/material from 5.16.6 to 5.16.7. See this package in npm: @mui/material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * Create .env For easier setup, to follow readme installation guide * Update README.md Installation Guide * fix: upgrade @mui/icons-material from 5.16.5 to 5.16.7 (#134) Snyk has created this PR to upgrade @mui/icons-material from 5.16.5 to 5.16.7. See this package in npm: @mui/icons-material See this project in Snyk: https://app.snyk.io/org/type-style/project/e2bcd002-cb74-409c-ba55-fb6349df1cbc?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot * [Task] #84, remove .env --------- Co-authored-by: snyk-bot --- .eslintrc.json | 2 +- .github/workflows/main.yml | 7 +++---- init/generateKey.js | 30 ++++++++++++++++++++++++++++++ init/generatePassword.js | 29 +++++++++++++++++++++++++++++ src/models/entry.ts | 5 ++--- src/scripts/crypt.ts | 4 ++-- src/scripts/token.ts | 4 ++-- src/tests/login.test.ts | 1 - 8 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 init/generateKey.js create mode 100644 init/generatePassword.js diff --git a/.eslintrc.json b/.eslintrc.json index f09bcb4..c69a5cb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,6 +22,6 @@ //"@typescript-eslint/no-unused-vars": "warn" "jest/no-conditional-expect": "off" }, - "ignorePatterns": ["dist", "jest.config.js", "httpdocs", "webpack.config.js", "src/client"] + "ignorePatterns": ["dist", "jest.config.js", "httpdocs", "webpack.config.js", "src/client", "init"] } diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e31f256..caf06d7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,8 +14,7 @@ jobs: NODE_ENV: ${{ vars.NODE_ENV }} LOCALHOST: ${{ vars.LOCALHOST }} LOCALHOSTV6: ${{ vars.LOCALHOSTV6 }} - KEYA: ${{ secrets.KEYA }} - KEYB: ${{ secrets.KEYB }} + KEY: ${{ secrets.KEY }} USER_TEST: ${{ secrets.USER_TEST }} steps: @@ -30,8 +29,8 @@ jobs: - run: npm run build --if-present - name: Start server run: | - sudo NODE_ENV=$NODE_ENV LOCALHOST=$LOCALHOST LOCALHOSTV6=$LOCALHOSTV6 KEYA=$KEYA KEYB=$KEYB USER_TEST=$USER_TEST npm start & - sleep 15 # Give server some time to start + sudo NODE_ENV=$NODE_ENV LOCALHOST=$LOCALHOST LOCALHOSTV6=$LOCALHOSTV6 KEY=$KEY USER_TEST=$USER_TEST npm start & + sleep 16 # Give server some time to start - name: Check if server is running run: | curl --fail http://localhost:80 || exit 1 diff --git a/init/generateKey.js b/init/generateKey.js new file mode 100644 index 0000000..e2633f0 --- /dev/null +++ b/init/generateKey.js @@ -0,0 +1,30 @@ +/* +* Usage: open console run: node init/generateKey.js +* type desired key and hit enter +* copy output to .env add a line starting with: +* KEY= +* directly followed by your output +*/ + +// Import required modules +const readline = require('readline'); + +// set up readline to read input from the console +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +// Prompt user for input +rl.question('Enter the string to be encoded: ', (input) => { + // encode to escape special chars + const escapedString = encodeURIComponent(input); + + // convert the escaped string to base64 + const base64String = Buffer.from(escapedString).toString('base64'); + + // print the result + console.log('Base64 Encoded String:', base64String); + + rl.close(); +}); \ No newline at end of file diff --git a/init/generatePassword.js b/init/generatePassword.js new file mode 100644 index 0000000..c3b26b0 --- /dev/null +++ b/init/generatePassword.js @@ -0,0 +1,29 @@ +/* +* This is used to setup Passwords initially +* You can create passwords using the same logic as in the environment +* Prerequisite: You need to have KEY already generated! +* Run the build command from the package.json (npm run build) +* Then call the compiled version of this script using the key as environment variable like so: +* KEY=your-key node ./init/generatePassword.js +* Enter your password +* Copy that to the Environment Variables and .env file +* USER_WHATEVER= +* followed by the output of the console +*/ + +// Import required modules +const readline = require('readline'); +const { crypt } = require('../dist/scripts/crypt'); + +// Set up readline to read input from the console +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +// Prompt user for input +rl.question('Enter Password to be generated: ', async (input) => { + const cryptedPassword = await crypt(input); + console.log(cryptedPassword); + rl.close(); +}); \ No newline at end of file diff --git a/src/models/entry.ts b/src/models/entry.ts index 799177b..e040c77 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -1,6 +1,5 @@ import { NextFunction, Request, Response } from 'express'; import { checkExact, query } from 'express-validator'; -import { compare } from '@src/scripts/crypt'; import { create as createError } from '@src/middleware/error'; import * as file from '@src/scripts/file'; import { getTime } from '@src/scripts/time'; @@ -138,12 +137,12 @@ export function checkTime(value: string) { async function checkKey(value: string) { if (!value) { throw new Error('Key required'); } - if (!process.env.KEYB) { throw new Error('Configuration wrong'); } + if (!process.env.KEY) { throw new Error('Configuration wrong: KEY is missing in environment variables'); } if (process.env.NODE_ENV != "production" && value == "test") { return true; // dev testing convenience } - const result = await compare(decodeURIComponent(value), process.env.KEYB); + const result = Buffer.from(encodeURIComponent(value)).toString('base64') == process.env.KEY; if (!result) { throw new Error('Key does not match'); diff --git a/src/scripts/crypt.ts b/src/scripts/crypt.ts index 6bef0df..3ce03db 100644 --- a/src/scripts/crypt.ts +++ b/src/scripts/crypt.ts @@ -12,7 +12,7 @@ export const compare = async function (password: string, hash: string) { } function pepper(password: string) { - const key = process.env.KEYA; - if (!key) { throw new Error('KEYA is not defined in the environment variables'); } + const key = process.env.KEY; + if (!key) { throw new Error('KEY is not defined in the environment variables'); } return password + crypto.createHmac('sha256', key).digest("base64"); } diff --git a/src/scripts/token.ts b/src/scripts/token.ts index 9a012f2..4dc99ec 100644 --- a/src/scripts/token.ts +++ b/src/scripts/token.ts @@ -44,7 +44,7 @@ export function cleanupCSRF() { } export function validateJWT(req: Request) { - const key = process.env.KEYA; + const key = process.env.KEY; const header = req.header('Authorization'); const [type, token] = header ? header.split(' ') : ""; let payload: string | jwt.JwtPayload = ""; @@ -78,7 +78,7 @@ export function validateJWT(req: Request) { } export function createJWT(req: Request, res: Response) { - const key = process.env.KEYA; + const key = process.env.KEY; if (!key) { throw new Error('Configuration is wrong'); } const today = new Date(); const dateString = today.toLocaleDateString("de-DE", { weekday: "short", year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); diff --git a/src/tests/login.test.ts b/src/tests/login.test.ts index e7a593e..0a934bd 100644 --- a/src/tests/login.test.ts +++ b/src/tests/login.test.ts @@ -81,7 +81,6 @@ describe('Login', () => { it('test invalid credentials to return error', async () => { try { userDataWithToken.csrfToken = csrfToken; - console.log("csrfToken %o", userDataWithToken.csrfToken); await axios.post('http://localhost:80/login', qs.stringify(userDataWithToken)); } catch (error) { const axiosError = error as AxiosError; From 547a7df8a7c2b2ec7d18f06adfc085ae89a4357d Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 3 Sep 2024 12:29:00 +0200 Subject: [PATCH 199/206] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f415bf9..eba586b 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,14 @@ Before you are able to login and use the webapplication, environment variables n Therefore you need to create a file `.env` and place it in the root of the project. This file will be filled with secrets to protect your instance of LOREX. -####1st. Generate Key +#### 1st. Generate Key Usage: open console run: node init/generateKey.js type desired key and hit enter copy output to .env add a line starting with: KEY= directly followed by your output -####2nd Generate Password(s) +#### 2nd Generate Password(s) Prerequisite: KEY already generated! _(may require server restart)_ Run the build command from the package.json (npm run build) From 1ad4c36e52999d57cb36489f93509fa17d03090c Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 3 Sep 2024 12:29:45 +0200 Subject: [PATCH 200/206] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eba586b..5f8b559 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Build and start can be combined using ### Generating Key and Password `Error Message: KEY is missing in environment variables` + Before you are able to login and use the webapplication, environment variables need to be setup. Therefore you need to create a file `.env` and place it in the root of the project. This file will be filled with secrets to protect your instance of LOREX. @@ -87,7 +88,8 @@ a;` ## Using on Production ### A note on security This application is not developed with https built in support. -> [!WARNING] It is advised to run this application in `production` mode behind a proxy that uses https for security reasons +> [!WARNING] +It is advised to run this application in `production` mode behind a proxy that uses https for security reasons ### Getting data Similar to the section Generating Data and Calling writing route manually, the application relies on data being provided using a webhook. From 59d05376263423649550797013ebf1ddc0c36063 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 2 Sep 2024 17:49:41 +0200 Subject: [PATCH 201/206] [Task] #123, add option to send testdata to production --- package-lock.json | 20 ++++++++++++++++++++ package.json | 4 +++- src/testData/createTestData.test.ts | 13 ++++++++++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7843c4e..c241ae7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "concurrently": "^8.2.2", + "cross-env": "^7.0.3", "css-loader": "^7.1.0", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", @@ -4017,6 +4018,25 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", diff --git a/package.json b/package.json index 02d40a3..aec3d18 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "lint:client": "eslint httpdocs/js/ --fix", "lint:react": "eslint src/client/ --fix", "test": "jest", - "test:data": "jest --config jest.testData.config.js", + "test:data": "cross-env MODE=DEV jest --config jest.testData.config.js", + "test:data:prod": "cross-env MODE=PROD jest --config jest.testData.config.js", "test:app": "jest src/tests/app.test.ts", "test:login": "jest src/tests/login.test.ts", "test:unit": "jest src/tests/unit.test.ts", @@ -43,6 +44,7 @@ "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "concurrently": "^8.2.2", + "cross-env": "^7.0.3", "css-loader": "^7.1.0", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", diff --git a/src/testData/createTestData.test.ts b/src/testData/createTestData.test.ts index 8838124..b60a5c7 100644 --- a/src/testData/createTestData.test.ts +++ b/src/testData/createTestData.test.ts @@ -1,14 +1,21 @@ import axios, { AxiosError } from 'axios'; +import dotenv from "dotenv"; +dotenv.config(); + +const MODE = process.env.MODE; +const SERVER = MODE == "PROD" ? process.env.ROOT : 'http://localhost:80'; +const environmentKey = process.env.KEYA as string; +const key = MODE == "PROD" ? Buffer.from(environmentKey, 'base64').toString('utf-8') : "test"; +console.log("Sending Test Data to: " + SERVER); async function callServer(timestamp = new Date().getTime(), query: string, expectStatus: number = 200, method: string = "HEAD") { - const url = new URL("http://localhost:80/write?"); + const url = new URL(`${SERVER}/write?`); url.search = "?" + query; const params = new URLSearchParams(url.search); params.set("timestamp", timestamp.toString()); url.search = params.toString(); - let response; if (expectStatus == 200) { if (method == "GET") { @@ -48,7 +55,7 @@ describe('test Data', () => { const lat = (start.lat + (diff.lat / (entries - 1) * i)).toFixed(8); const lon = (start.lon + (diff.lon / (entries - 1) * i)).toFixed(8); setTimeout(async () => { - await callServer(undefined, `user=xx&lat=${lat}&lon=${lon}×tamp=R3Pl4C3&hdop=${Math.floor(Math.random() * 15) + 1}&altitude=${i+1}&speed=${39 + i*2.5}&heading=${262 + Math.floor(Math.random() * 20) - 10}&key=test`, 200, "GET"); + await callServer(undefined, `user=xx&lat=${lat}&lon=${lon}×tamp=R3Pl4C3&hdop=${Math.floor(Math.random() * 15) + 1}&altitude=${i+1}&speed=${39 + i*2.5}&heading=${262 + Math.floor(Math.random() * 20) - 10}&key=${key}`, 200, "GET"); console.log("called server " + (i + 1) + "/" + entries); }, 1000 * 30 * i); From 33d409ec5353cd05648d58123cec688c199deb1e Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 3 Sep 2024 14:34:34 +0200 Subject: [PATCH 202/206] [Task] #123, cleanup environment variables --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index caf06d7..1fcf58f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,8 +12,6 @@ jobs: runs-on: ubuntu-latest env: NODE_ENV: ${{ vars.NODE_ENV }} - LOCALHOST: ${{ vars.LOCALHOST }} - LOCALHOSTV6: ${{ vars.LOCALHOSTV6 }} KEY: ${{ secrets.KEY }} USER_TEST: ${{ secrets.USER_TEST }} From 0eca482b728a57ac35c06b9578f407d75e8c1344 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 3 Sep 2024 14:59:28 +0200 Subject: [PATCH 203/206] [Task] #123, increase rateLimit --- .github/workflows/main.yml | 2 +- src/middleware/limit.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1fcf58f..269894e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,7 +27,7 @@ jobs: - run: npm run build --if-present - name: Start server run: | - sudo NODE_ENV=$NODE_ENV LOCALHOST=$LOCALHOST LOCALHOSTV6=$LOCALHOSTV6 KEY=$KEY USER_TEST=$USER_TEST npm start & + sudo NODE_ENV=$NODE_ENV KEY=$KEY USER_TEST=$USER_TEST npm start & sleep 16 # Give server some time to start - name: Check if server is running run: | diff --git a/src/middleware/limit.ts b/src/middleware/limit.ts index 1301071..73b8b02 100644 --- a/src/middleware/limit.ts +++ b/src/middleware/limit.ts @@ -56,7 +56,7 @@ export const errorRateLimiter = rateLimit({ export const loginLimiter = rateLimit({ ...baseRateLimitOptions, - limit: 3, + limit: 5, message: 'Too many attempts without valid login', }); From f8a2c59c6d22c82d3faa0652cf84c2fc36fb8497 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 3 Sep 2024 20:43:17 +0200 Subject: [PATCH 204/206] [TEMP] #123, repair test cases --- README.md | 5 +++-- src/app.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5f8b559..d6c868d 100644 --- a/README.md +++ b/README.md @@ -100,5 +100,6 @@ This application is designed to be used with the [OSMAND+ mobile app]([url](http Due to a plugin called [Triprecording]([url](https://osmand.net/docs/user/plugins/trip-recording/)) Using the above link or by [clicking here](https://osmand.net/docs/user/plugins/trip-recording#recording-settings) more information can be found to setup webtracking or "online tracking" - - +## DEMO +At this point, there is no demo, but here is a screenshot: +![Demo LOREX, markers on a map in berlin, information about speed and distance on the right](image.png) \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 20bc2df..a06be02 100644 --- a/src/app.ts +++ b/src/app.ts @@ -77,7 +77,7 @@ app.use(error.handler); // init server const server = app.listen(80, () => { - logger.log(`Server running //localhost:80, ENV: ${process.env.NODE_ENV}`, true); + logger.log(`Server running //localhost:80, NODE_ENV: ${process.env.NODE_ENV}`, true); }); // scheduled cleanup From 786f9e4efafe9054e7e59ad35e2d1c1677084e5b Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 4 Sep 2024 16:12:52 +0200 Subject: [PATCH 205/206] [Task] #84, update readme, add demo image --- README.md | 2 +- demo.png | Bin 0 -> 371501 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 demo.png diff --git a/README.md b/README.md index d6c868d..2bef9e1 100644 --- a/README.md +++ b/README.md @@ -102,4 +102,4 @@ Using the above link or by [clicking here](https://osmand.net/docs/user/plugins/ ## DEMO At this point, there is no demo, but here is a screenshot: -![Demo LOREX, markers on a map in berlin, information about speed and distance on the right](image.png) \ No newline at end of file +![Demo LOREX, markers on a map in berlin, information about speed and distance on the right](https://github.com/Type-Stle/LOREX/blob/dev/demo.png?raw=true) \ No newline at end of file diff --git a/demo.png b/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..f012a3f96116956cd8853dd91972bebfa00c0458 GIT binary patch literal 371501 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

xzonOTe?FXLO7$;lott*1#N4PGjR^ z_D&+gp>1P|BlQV8bECj05qjHc`+}+tXqyEQ#V(L9_;goZZ6_bpWi5OU$1W5? zf6u=BuivyTXU6vAKR?xdyf3Ch}>h%v){?q5Wl4Q5wd-CQN@5)_syYj{d zcIwCdRT{YM;^t8FIDB8udK@GP*vXgqV0xz?;Y%t$N#ub{(}+GJ$cXTs&aRw zTR!&jl02p)!gd(sQbt+Y+Nl&F5AY63mlbU%_B4>SU=z%LMprZph!Wq)cRDVf4YmZG zcKj+vIvjB@n8)5PjpdvD2$$1DL@oXtc+u8CpJicY*$%0-!)+Dh2x~N0m3SlY` zTk?7(V`>Sz=lPCzMSyJjXVlp4@s6|h%QX+>fs*VjROQOW`gTG0-g}RI7Tq$Nv^jYy zRzB&vLoYoJNqstH=YS5g&I{ai*IinHeQWtvV6=*cv`G1&r=;I|qAs8LqPjww4#Rg61OIA63rodhV?H2hsf6}fUHwY`)AIgqam+jxMiFk^Uwxdg$+Rd z%nG7Mzes(=HjQgpu#S-_p;@r0mvq7=Bx`P9Ls%ifD+j25j2`1Z1|O17-XCgf)su7J zC8w(L*-P~qBmvC2`Ul^*E)UFAj_*K6wYfVs8e!fP_4;h_}J^IV8aSL&8Z6#J!B*$I;FRA*!tMl+s}O2p5Tm z%>o$ZEK)w`CVE1+UPr?g_`Z1n^&TUNOovC=7Lnm66E)o}`kUsUNTlyhg+V=4sxgdK+!m$B3747Iu z7RVxP#o%O@-!_88my_D$kUzA^%d3J9XIDBK87j zdURyOMuQJQ?HU~)vHpjVENnTtkAB<1Nx<}QeJ&nR_*0K;$q&75(`v!zFV~3{&;9kc zZph2dROQi&W%=vWU#z(} z4r#$TYzORSxTVf>M*DOCY%J6Nu*t+h+^y^zGG|nP@EMU|Rs{rsMo~RejguFwJF%iJ z$M07H$T7$?tMNQ(Ky;{EZr6*IXAv-Rha8#_Tbwf-e3gJL?k4{uqsit=)N{RptvPP{ z48g*7v)%mCV)kez`6n)Kli4o5q{QJC7!gh?olUP(LeOj))B$)#@2hU-cZ4#V2OkJI z7irIG)m@v;1j({CQGF4-L~GmhI^o4(K(ZLHVRlhjzxZq=hz81@+-G0i(S6^`)X9IT z%ln~Is*}pG{#XeW`14A<-f-8hJUL&Izx$hmvb%S5J;l#N&+z*g#bP8isJ*2nrw))m z9QAf#qhv^wR@NXw5~8&2u(Ecpd;i=NtOrG^#j>5U^=STl`9)4~il+Kp0o zS+=|DQv?|W=6Ve5^DRfRV$iU`68qF)%W@zo07kYjHZ>Dw+jg+g!lSpA zMwx?!p@fHFr@_F%;FH6hM@B>j?0T!phclX2!EP2XvdELXK$J*hDl5+w8jQ4QQ4x-&z>k(#a{NB#kUM zK?r&A&sO#e*n11Ld{`^mtiu3XO(zRwiw&jTu{$by384<_GzkM8Xa^OwExWu#zC~Sf z-CMKt4r`fWqbZwoeJ9_H{!UFzs?_(b9bq?I(#bArbc~UHY(-sWbu^NzboK%$~*P02G*K36JSIrRW0izQT%RD*b z+d!5nOX}Uli6)3m{Hn8EjJ|J7jmev?kd8;=$!KX(9 zi^AJ|kI(C-pLhBDZ;F%CoXa92*mQeVzlZDo%h3s<@Z@t9`K4wAD?%(kYjZ=SIdOBZ zrLk$^vUNI7ek{{D*uWx@O_uy1h0ZT{)(IB!j&Wm%79=Zy=EsTFzRYiseA1STa)t~h zApPYY-V-=>YmzKLj|&Yp#+GSLc_4loR2pxL4vq@nvkoUs;ZX9V4J^-wBakh3{G3UL zBjow{4hq_x?JjdL$x=I`TF42-@~I$RfpD&1_-ThG4R#Ua-67?EZBUAHokC!$=j!6R z)OPmNG3&9MMktBD|AM&?k`LBbX+hcuz$d37qH z7mzLPM~CQ1WM;&GI-JDK^ft5il^$Eugs!nRi>cf`jXMb_1IX++0MiS8#>IrogSst~%LJlY+wn4^r5LOyl zGJ^C$4oB1LvB@iml0t<0fr~(_$n`lj);anr1zRsLheo`)x!{zrOorRr&r` z?FuU`{`PMS$VZ;6$m7?_^13^BZA9xur>e5D-6enH*M`&~O%%KX@TqSkMgLm560)HG zGoiO-WU~;6!(~MI;R7NIZxhgM+YbKVp2NV=_Bv0w^Z#Z~eRKH{4`jjOB~v-DYnoQu zRq_ak;Bp>GIeGcjNfx%BG*JbXw%J&(*E=9`WJn$c2S;vQ zk_9jx^w!ZEQN%$Eh4MAD2J)nFWK@fh7k(FUWN?ZhRIIlF$Q);DV`Dv2i2~?Zv$^DB z<&IAiyz=xlSzBJWwRNnwW2eYKumJgjP|^Ukw=OFi_#EPgj5 z%8+34-K>0CAuqpKKu6Gzbe+X{BONsQIPP?w@X2&7)+#dn1rkPFloiv}AYB|xdBJ%Y z81!j%p_kAae?^hJ9nQnPIxCa<>|QR(;vPQ3k&Y+alLx;J>i5uKLmHW)#wY;2&Ha5o z^_Y&O`1K7%K|HkIAvz~m%|L!=;F6yi!1mH8htbIZo0>H;W>du=GB}8f3yW5^hiyom zWKsU)u^pgGkcf62cJ8N+urUN=0r@bU83Kmg;}iWPP2t{FI7;;)-neY)#5a_Wf2Ij> zc&-_^3`t}($-@re(7@Xr_Pt$Drp!(#YAcD0y6K$}don-2XfG9}4!7H>g>HE#&AzSS z%j$3la>>dgG9cX$|KWj=8?sKTy3@Kn!x?19#te?ou)1!FKlL?>hYk; zElim;`Ke8#?()e9Z7bk3j`V6fqR-MvX6v6cEX0kg={@!$?|SL3{Andvn|kZt_|;x{ z?2-}?y>?}_B#&K%XyHqUjbNRR-sjX2X#Dt`lruj_Q?q)~j09!jaJ@s8d`3h;%c>q` ze%RQ;gAo}BwuTj^zQt*}p+xIIex8@Z`E3O9LkHTGVGJI{E@X#MKgaY+C!tP8Lpb*O zfJ5y{9VaV4G~a=3wJfNeTd($7yWty%8y^b$XH!iH)7R4q|Vq*RVsZInLIVF z^`QG;O%|Z>2`}-aaYA?Fo$oq@vGEBtO8bQ!lp{gI4~{GK1HzFvRg>Os9C>={kAqB_B9u!9iHxRA+dXDL4K;7qzIC9~+788g((tyq} zk_btJG%_^}!cT8Kd9YD~?y?dUe+>g9i*)6cXBuvYySkep!Uz)(5MIKq8^MvqO zlSTQkHTTriv_7K*QRRu_z~~k4Rbu3pG{=XNoVs0r?6-?Pm05QeWq-6zJklLVR8~XX z-_PP@b`JEH+s1N>e;3jFfVfP&JopoiEKqKZaR-F3{GP_^*e_p`=`s!k%i&>$_|wle z1NOvr?JGESl{$yR#&k;vaar}A9fCuh3uVv>M&4CNs@k>?9yTP6#(enqth{Y)Y?&RR z-fe82oGmUwumX5bSE;whiIx^BQwXb@$%}s5ggbcHcAu)tCpyacK&q&NLGDFJ7U>x6 ztvV6VuxuuQ$rfAZcH*5c-8O=?vC}1g^S}4Xm#&y{9yq$!)|6;nDaps5t;iRywAZ1M zhpFB@d0=o~PWRT2v;AH6*{5c5v59_Tqmk}@d0$>Ryl?NtN*`l;nr@0j+XUJQv+MfM zRNtOFr175V3-RvR!S8`;mn=lZcHdl0-gQRhJl`er$vF^@$aveJZK%qMBSz70GIA3-`&%5xa(CK_#a*wiCpNcO0>8F5AI= z4JQX0R^GeB_PtroNB|=zEM%|MoMf?X!!{1kGS7wTU#+Vu%hy+AWMV`H$J(1Rlm|%E z{KZ9?IX5Mxo|0U9mKT9mW$w0F=~DY1euLAe(a~{PTwF3T*;DZ}Y-t0G1J882FgkY} zumv(BeU~q{zvXp1z^SF2#>PiwW_mVz_b5dmVaHkPgZ&7-)c(8vJ#9B2H8lF^?eZ~1 zOx@xo+U(3~6&hM(zrxw`Oh?VWVd`_+jGd7Hzt7&6f>So8S znr+kpd;=3-|3+T=4hS9L(MjQX^7R`5jB4Q= z@{-=>`CK&dr^&@4QeJGRh_qtm~jLV?jAn?061>+NzTUG70%@ z7t~eSl=RMWodR2-+d3k4xm{-mO}Aq~cI`M9-Y)w6?Z`fmp%7Kxv+1x&8|hzV9hu6% zS)4&1ispxKmlhXQ9?eJiurF-uUMlsN4MNY2EGhZhP@+SfWQu01u+%)r%Phj5%)C=d<>=J|5a3SOH9zr>%l;)p##t8~POk8B-^*6#!ep zyXW#o4xc;!Z1OFISn(96hc=|5=|lK@V&9NEq^qoL_)mT3hD;8M{Pka{$XDW*^(t_1 zJG;I1zI^Z3ugZ6vQ<9gQIfT8EeBhV*<)5zPJGyUvWL5tBi}xg#@~Pc_^h5pfx1KJW zZ6n_A(RjbJS-iDw`M`hbmH#7IF8Ez<+mt^w+9QAa7s~P(ZFd0E33#> z%_29KmxiPA;)>0Pg1G8;M%k+7-+7uksei=FL9F&^hqmLI9ll@N9p^6LCE=uld}FuL zn4UhpRRu-+dk>70FOV0{(`Wa5cp*X5B!D&LMop6A^hI~*ylqpVP-qbU$(bFEL z4^h@dwr;0l^pEx8>&oils_bm;%E0jPiPqI;omdP{_}Vh236c-OADJ7HjfMIa;+!8D z9XBV6Mfo;Lvc78PasJR(dhT%wFgQ5FM**W0Or1m~Ckd~#7@vI^pP0;@o=A^0ogG0A zM}gbkxc=6^yJ2xBkpp8xRi_s<26BZEwL5(XpqE+pgLC8yeGQ`~kV(pxkqHPAcEdV* z_Sx-c+wP->ob*8y(dlhoAU^ZA}(-h5q`o zmPN3G5QbgN^Y1;?BftOEZh7K5_9viTCLJA77}=2@dQDa4|7ci#>i2u)BafvH<;^d@ zQ+4fpMLxLX{tHJVs`5X5-H!A<(JMdpfgbtr=gW=97rW%{hicMytt7v489mZ4Mnk+q z@{_+?mf!hYsqvXB`||KT`?B?HSw3W9-IupLT$S1DJ@S)JY5Xcjh&_kxJePFnQQRj! z_3g}#eBa&s^5kcF<@Z~;%o;wngK}>=qNS&>|9&MuQG^x3utW1f6Gbc99EZcS=k*_U zImiq0M0;&%438kx=e$j9 z8N|RDZ02M&12SkO`vB5Po+;NrF8#X`ERIk*K!p1ptgd>dF za4%tSK(+{z19=968q)0YE0%ul!->N|jrEi;I4ExqD};} zx`v~*VdRP_fpnPJZ3hT>&jX9_j|ybSsN!*VtII>EN!;Cg z^6ppd$e(>^U+&&4%cs^TvmN>2Z`_tQsDJ*t2W#@K2btQpFRvWXIa^2deT;mGRr|smeFpyC*NIl{D>L!kg3316P%NorgU6 zCNA}R(67*r2GH5(gw=mrSrfjNdrai`9sH-GAghfRnZaz?hb$_(%$YrKW_((07PtNdfJS)&Dksn93_7--q&!_pqwyNI2~D_+`N%; zWuZ5%)mjNE20J}Luu5vsN9TN7Xq1kre#_U}pT@>BIJZnw0eky3SzBuC#oJof23{SI z02&`gG9XNO2rs;3jR^;iH;zH2w_o*k)TT<(VZbMZje{wcb8~ZY_Uu`inVB)ClzYyc zIb-LhXJ-A-`&iF1IXPvY;kzG`2j3?qChWj5_0KVjdm_0GsbxJ6*T=>t>;S&2CL?nG z8UjcSjVO&L-x+>4hdHUEG>nY8krxP=H@4aEQIpo75oe?`%aLKHDjWmghSI}~zaIza zCgpMJ)M>LZl*zes=S)uI&&g*ap+^tFQAaI{qTT$Vrx;`i0KjI4Fw%}v7JQV$$y!lx zFET`jN9c?w^&~HdcYDsLTtTkUfV9lpg*oFL(2qRq*p7mX$}@84J6~SDblIjvL$F5C zrBici>#}WPz)>6YfcWu1wgN(VWO`;%Z2tMOmF5l?F!G);Tk}=VQSQI@P=`9TJ_(hoSB|4 z8`pGgbu01lv{3Aa;+@=+aSh{IhKfQ0Cnl;grq{17Bs)xNIB%R#dqBL`{2n?tV;lbP zxQ4Iso=^G$_9m-xx~EI7&-*>+rmOOgzH3w7b!uNe_Jj~4SB3M9zLh`a>_5 zGTtp;xTxo*Y7Kd@cAR>p`FZ18l5jPP)J5cH{J><`SU1s|&|%yP=pFu8T)c#D@kZeq z;VKef3qY{4<)4P{O+aS^ekpaPDC$~O5U~SL>gksLQQx@L5ZUeK10^~2f|-Nz z+*sMLwNsJmGzIIhcxLkSxF*QM*jhiV*z_#)^wk?cuG2$Gr7qeqGO}f)>*^I-V?@s` z1_(k!FF)(Ja3bdCuiLs6)*GSkI6W6HUbJ;JJZncn+*r2|e-FRI_w+%$9E8C-FTNA! zqB<@YFJ7?YD_?mcNr&GXh$81gT=kKeHSsO0SL|NuBnA@lMq^H+o|aD$#~z>`+-r_; z3S6V1i5n{%+paE*I(6L1RyY8k+>;kSb`<9Uq6kr?Tv&s3?b>xYfBwA5D>!Ye37VUm zHR3yW>Wr%qRh37h8lRF0Ti?Q~pYc7i-Wsr8nn>yzasPC*-gq+eT0@x1ws@iPo zO;&jpMbWy}4D?KPBAhI2t4*NMW|TDsgp1z)>#9o&^NZF|F*QBq&aOI^S!jpIq(K_e zWkq-)O!PCWd@Jg}gsFAtdT&3QtN6^30KT{5@IoxnzdWEXbXwr_GYzekPr+11>=pLZ zW-e&G=2_|>aj_FDgF6tREQGYD^PdIe#eHd_MHuKat2xGo$C9(h14OI4LMI|NtFs1# zPA}>s2ezvUnN^$ZuNr9V6rvr%PS7SbO9q{9IJ^Ox>Vy03U54t9mC$WH86-{J58G|~ zR5x_ZuM(#{uLqy*M1#a%YnGS-xP43&O9{hQli!WlmwS{9as7+QnJl7ZL~XAm&nZzO zO|<`^tZ>F(H?8*>Q6m#}T5ccJa95IeIj#5Ci9(m$JEhO8bjf3Ox5hi8BtJpIfTgbWZb6^f?IMM}Mtf-v65e@*^J@kbnJEEswpjd_>Rv zlP@Xp+tIXt+V0zvJ0|vJ<6>Xx zz5)5wi<+^ulGsYUT3MdZsyC&ZhTkX_d=0B`n?|cZGI7VyJoE7lVh*qOCS^xpWncCf|>ai<*V|~&)RIx~zdUNgSRaskI zkscKc29}L2Fe1&sqPx6??Q7^P!j!qRQ$yuLnHi<%vW5(j)n|mF8M?GF6`Y0YzU-{+ z%EqNF*;%N{?CrC1>h2jC81d*M{l<7FKXM232qE_W0_D1GV}BxTiPMT5tA!Vc41$j@;c)1n9li};QT(odcsT&yLc z+%1aBzk!x-z{P3PBzaF3G2nfBVF~X?T?ChKqky4MXg)%~^ zWR!9N`E)87KY z`3w$b?3{V`IZsKjO})A_O1{;ki%`vOpQ`?Rhzd`}xFa2En1k4lOOr>+hP@;Yq6} zpqUP&0hwg;iM-uWDF>#K5+>8tigdCC(18bOttz9Uj@jszI$w;`udn(hDtX`-va-K4=f~$N1EH9MgE6Eu^8p%c=T1M2?yX7fOhdI7_j=Y6*o*mN*i(T?jY&2=@ z(s-4?>?wJR<2@h>X4~ zdf%F!->(ye7b*!Pea}Wj;SSQY(yc@(XGCzaCX*`bOIJcD0edo|GFey(dR>$0K_z3? z4%^_{lt}I^m*hkCt4ki&{8Xl@@{O8~Kli#_89P59KlNoLmXlTa7HtFm4CgLXiLnX5>!&+l(9H$W7plIf1-ww@`r$LFK^54@}3Nj z4#~NPPRrRBo|4gNpF*;-up(E_UzbZ?y<($WOb=YRv}gp3_$E&I8jliTEB4g^iOIkL zx*OBRw8HA{wmL`0+jIlxAUN__s2+p#w1b!|lDDG*nbLsMOM2(`B7&uOOJ>Wi))!OL92=Spa=n{nE^y$+wIi(JWrU^Y|+6=@BB2|Rgl2@EP z7UOe2!m{PAfzOu_26ype_*1@dldN%|Y>VHScDjbj-*)OBaGv1Q;l z#EG2@nYIeCqU@O_hm#&xX>{F>#s;wQ1NLu%WWOcGYJfN;lEMtt-i zS2-YBF`>3LAo(6(@6|Fx;TsmPF5h(TNaAsVu$cFrt-d{(QnRzLc=$SH>-J*biQ00v zeAqA52|usnuV|FNH;vm6v)fz4TVRPd?^W> zx3nn|GIn^XWKHJO)r#}@p(`cX8LY~SUbHE1n=Q$Q|Danw_FP$(`)YE}i?-$Mr@G}s zAMTbf(Kx7_?qw9wh#)_GO&>nUDTuFN#00TAAQ6{a?U-hsxcr-Z$?>kA+QAnWK{-kP%_;8~wCaTrcM zyrH@gh(~u1dMFEmh8u#hu;9}VY0PmPkukEP2ldL8tMb%SPurA2j0a9RjdWGXNV+`9 zI}Hxk(x437K{~7h%@D#w7a6r8enuiUH&%@lk#0t+jsn6p-LeJw{&H=mG=(XdOaWzN zkH!P%&xqIb>;dt@nc+yce=)e0MMjH)DQl#Yby6{0&yXp0w@#w+ob>oSjX3LLeM;D_ zO=-pH&D-Q)HF3BZ;b;crl7+bb9TkLe+>VhvSRL}R@zN+39}MUbg1kUr`*HkEX-2CCI;5E~g&+m$yvfwiF21xPtMb5xKH*2_Zd z(PNzb+U}N-R^mvHK*eyG6(d@Cpic7&5?<@DSKT`@(c-x-9QqdIuo>t$f~d3mBIFf& z#I(lc#Rbi0@s2Y_9~o)Ee&;U3P`Rq~5TgBzzX$M>jXk*9t+t0}?7aQ0BdcE9Jr;TOh?k{yj=RaNnkk-fvmT~(P?7xhZy&AEQSgzuMI}I%WIOHYtB*XX9qQiq(I_L2 zN?w=>j0__&zD3DX_hz@O4W`m|wk8u&lJiVA1k*~gG_9*Usr#m?a#o4t0T~0&l;wh^ z=k*V3-Tqv^{EH2h-<7gFr}4k(ReJ8Tz4BA*N`ADy*7kOdoGvOk4KHM&oK4<(K8yGU zkZpf~Z-`sZ1Q8{GtfO~UhUtq6+Uq>6vS&M?e+f+VsUV63X`LBO>{vkCy9+i0_ZzuV z0hRj7YMeX_oI`hHnDXO`Ir&ZxZg^oQ1;QfiqdHVyLwi7p>78b}QUGN^FD^!$@?eSw zJ(+1|J4Sa|^+OFd+uiQB*ip&!K#jrfT2+a7#Wu+(S4uK4(l5i~LozVbZ&hbk8~Dvt z)>f6wxmi~N)(3f4J=t8Vk1nB?JiETMaZu49?WCP;=b2u16p(Mg!tgZEXV&>Z!m_S+ z8pH=_^zRVi)@vLAt0p9pQAEP%=LN%>&H;50`^~`) z@9yw2p^P|pRG_m=hosB^vVp96qlTVi^ob)I7QHr9^oUY!Ls~}Uhgk%CwgL?WWRg6! zvcKp7GK$WMnKMD&Cg5t`UZR5*PKH>14bMjQT`b@)BFt+@&!-K59E_rngTAG&*u=!R=@+(QY~0s`rFE$h}F=!Jug;OxC?%Ybo~_q3$48pMuTW z;(l{@V?1(ZOCG!I?Pr$~1JV|3O`=`)o$FzA*{}26I}Pf#nTPa#!tz^-LzDZ+3wP!I zb35|J2QrScd$Rgezx>-TloCgOPrm)t+j4hrNk08Y$RlT)g*Vt;_Z`v{iulT$5GyscfYzOTTk@MZ;+7PZu#h! zDze?bD{p*cTfTmxBp06Tmgl`a)cKUPHD|4%!{dx5k(5 zKt}Iq2eWj_?j(HwreP)eXW#8iQ=qy*eO0R)zw@boK6j~0Hf?voJZMB3FFtBVUq-yU zn?J<>Ah6!y-h(Z*sh^d>K3QL&0Vu1%nXt_%!-uY*rG6RkBFl~z-|5jiF6j0ke+z3(58G0w>{<2g?N>bZC{7iTO zF&G*fvRws7r$(e-Nf_(80?@bFJEm=#A){abJ;E_&6DvJg5GLC+^iF61x73a2?*d>4je{EllAIrBj4S{i*>7(m-I8#dv@(Z zp7cOtG$8{_-AjY}kUdUB8W>A}En&2X0Fyv$zjz>vkbGVRK^dWgu~E*_nRbDGV^b*0 z<09B7WWgvVBs)y4B|i|VvQ0RA;^bp#{jh8EFn6y>x8Q@eoFK;t`&_(H?Yx`s$q)YX^EsSd&vzHf1yo z(!}njw8vMkUTer}yRZ>pUN52gi@t;rRN@#Jo0Ne*M(38b-RLtS$lAa>@C-=Hs};eb zu0n8Pea7UK`YN`WMNzQ)p_aTatG3PN2KF@<*j4HSda=bj`P5d>ah2C07s|9b>SfHR zCF@J+!C%yNA#S5+%K>Fa8(vjn3&Em1VtoBQ>vI3pj{M?hG`&R4tPSjrriB+01JXsO z4f(W5gI-HI5@>((+1ns#}3I)Zw>lKynx@tQJ15WHA|QOYzlqc4#2D#_#HoMdvrPYC#xP7Yps1P z(9xF>w;IVJspZ}t6Po|hOsGLac?JiDZOeD24TP{s2)#NuSr`iv*ryFG4Frvt?{pIc z`89MJOfR7UMmYd4APwJW^ z@*`P*IzqlUnvEJzUm!VgouLeHHjs(!7N+GIjAU@)xrb{Iu-JIcz0eIc#|hC}O{mJ7FLT8f&jVKAPon$!;|uSBwq} zXxv+BKN)@4)-Y04ttGghGC~iq#p${V!Nfr)-{A;wojUqdssLn(19@^IS-qAA2vIwr zo{Wu+tFA6J$ThAT=vv=^>VVcU$}W~qHnPhz22SpT62sM%HES1>v(abzd;P%4X8JGX z>6>@(zC30w^qbAfst=4NQ;uA3*Jj2*7OXkK4luDHezz9#2GW6UF*Obym-G>fy?5$zxv(E+}P7<`eQyarSyQc-nXCN13nexm7 z*WJ$7Wn<4Pq4G0)Pk!gf54oA_3Z(5sq47h8j5zaC(-fzf>8j1hV8{pZhzS<3-}*mZ z0s;sR{Ue$XmBne%xc-GOL-=VMOS$q)J0q%4Z~t`A*}-=)drRZwfn~t$XET#Q_ck$LmY%1lC&(987f3Mi_@j^gc#9uiKO;WO`H>QBZl6%Mh)`&V!+mQCmxb zgBuHuNcqYt6;pxFixlsmGa9&o?g5qhn&8(rYKpAQ`A zLDiq-Wkx5O-y0(y86jQhRH%!TFXhE3lXaM=JeVGcjiAm$ve>RYYbs$;@R|U^!Dg*2 zyWn7(oR(XYWRb$20bgj^ z0FAf5H#BhUgC9FmjRnFd#)%$i63w~|8i$fL^7~&STO9?C8e;d}Fy?y?nZrmK;B?V2 z7jb&L{6Yl@M;)&8jpuClLM05X8afCRZ_j5-`AR>FsmoH)28M;Z3@4j&IM~;)R*S|5 z&=`%3j@y(JufX}h$-uE$T%^akF|9F(j~;g%5trM^qh3Zn!srfrBizZa8y`pzj?_^= zc^S#p@&U2a6{hv!bh0T5jXfh}$ONJQsjBppEIsV*M^83H#HSzC<%-eu_h%(14v;VV zc0f-#c?kM^(m=mxlpvGXE{raEN8R8+PK;KKDq#tW{~=eL9~vH0Sum>4Xg)SJU0<-p z5ZQ{%%*>iIy1BKhPUMmiqE2KF+l-y13`uWr*g)vXCO_C;@)cXJP4P2sUww2Nz2uzjS;W6qi(M#OX@M7p&kGgyUKy} zaeekeUBb#6QP0QWvT&Vb$|2>vA=TZQt?LQp7VHV{`YEeUV)uEbT|h8L)!vc@rYT|r z(g=Cv9_kEh$&edU(}K-H53|UYr#H`;tpnmSHp*JIMay#(jnm@Q`LX2dD6MVZ;u~&rG@A|p+D)*h0zE@@*H6`*oNrqiGp}I zG}*`~fP9di@Xzka0r!S{Q*Wui&0ryF*oS3R52udCuz1eM3$ltXnf&4t&6ChsXp&tQ z8q;1xenAd#zC#;mdCUN(n-Nt%@1(04EPVu)leR|qO(8}jS;;51=&8kG>)&rE$s%v; zESM%)#M{lLM*HlHv+Y7?D7?eWm+v&@7>KnsMkOjTJmkG`WTskhbmD8s2c&`qHV`SC zk=Q96(T1L$(AGGtSdBP*o#E*6ke0bsn&vg4Z$JsqNWYOOJBHMGQu4()4xVOni~t;L z^a(v-RF%fC*>>#6fW{K%xRqWpiq$_bth!m>3Gt*seW~|SGPnG2<)@B>a>Lodkv}eQ z0Er8-71kdCoIjil|DdLk(HSNFiE~Mv z#_30oS$kMCt%Nr*F{%2Tzi~%4mK*UdNWsV{5oA#OER%E~jSMc5?3Pj6&Pc&@rNU zKyKW_cc1#_U?dBY^`bNT@`X!!U85s@ZgEG~%basrpqyNvNrR`wNQkD_$~bwB@5skU zpnh-Wn0Fc>9ckU+H#*HH>Ca1#J)e9sb0GZA1<~m}_%+S+71`N+_Q{!~oDgcq0R;fM)`c(}*H47bF=H9Y1&xJLTe z&5f0AO_Bw&NEX=F^60Dd%ceQ)hZDj%E40)#3?|bH78VxGP+$bY^al(vFEK9K5PS`3 zc!M0s0`kNmWU2>cfg{k2oCKiH?E+=&87iwr>%&><&<3cy8!PnadqAWo5D^>*oCedC z98!d{NUv*=USsM6VKGW}BS}_pwtPxbhZY3w`?D8UUUt8%@I!P<}!4XFO zMSynypK_$ICO0sOz0}Y>ghTc=}NGI~54A*SLj*eqbPm!sNhY_ABUVhjMI$p>R z<-?J-b;xX2&y#jW;m8LFBnw^y$r3GNWMs0`Plzo^3VBMyon^gO|KSve@BTk|D0~n| zaz1>&QLuFBk!K@0Z|k&3R|-rEW$GvTe~{mtw4;|nKP{>jg-BQsx1h76!_r84h{pwX zaIgPN(wUsU5%4@`%TH5m=kw$T!g)Sz%^nIDA;`_|BOG*A>v%?d7GrcCyXeG?U;J^| zp~sxGFzp)tQHrP@z`t9l`ZXTOdi_1MTXni%on(>MvVONY%*3n0!=UpUgFu)`_97st z#`D%dsgr?1^7eUtd++p~8s9!M!nQ@K8cZDDtW16l*`T7+@C4(Gld`_CZpuR0Ga72{B#o3SL7Vxaf3pgt{4$unqY9$V&v-u^&B)7z8S}3Dg;3=g>i9l zK^;h@?lF2nkGO~LaoiYqRC)Pf+L)85t5>g>{Fo+1M-+}Wbq}YJGR1ji;};q?Yhcv* z8ysNT(twpW_f*s#;xJJ*M+LTEbacdaWMoteU1OBv>8E*@+Oj#8+{3}>9QB6tyibby z#ppLSnd|M^VzyaFmz$*@vQL_*&yXhcl7^Xg2ti;VDG-Hp7&NkU8ZqK)4Q>GthwF5) z`}2StAW}HSBP!b>#05D(63`DifoLuv7AgMPYv-7#-R)Hvqj6B9-t7MGSy*8-3m zWyk26jcTc`G1ame(PUa?7&&LV_AL2&ooik1AY#;k-5ss7I9y7ChSWwga>NusYwwft z$DcqpkWoh7hBOTjLq?pC4I`JV0YQdLgAH4s8{yTw(?+1PjLZ)7xsO3zV%j8Y9NFdA z{f#x#8|30d%Qxg|)Nkwx>G$vLg6G;PL+&MCq=AEUk-ix26M4socThW7c-wvJ`h8#i zt39hSwX;z?A$=ha>FXznblhp= z$uGcV;v;em_i1pnX+duA0`9TgaZd~RAuBpgkYkV^ejm^IZ#|Jj6}}_4#<2Z{@G~Y^ z58k?KvLcezQ<9za9m__E9pGxJti9^gchlpIF*uwU$KmrdE>ly}YDl&Z6dFTJ?0(~5&8%eS#li=)y>5u{G zse(8SCjr96@yD0|(u15CIiqKoG^QJR^e78v>p`4 z21mrn3{$6An4UNl>U5B=Sgyx~1*>}BEI&AUbaqgNeqa5dNJ%Fn4O|ZnlciGwr#!0K zu#`)TCaEr~vq?GNgklGggVs#DzbKFu&L;0MLQasXt5@x9F)e}Y89joW;rP<&)lS@W z_)t!es<)8*IKk3N+$0DC_*HEC;-tbB9whg>N?rd;As`-sWm(Ij+*mxu$}z^Rh5(e4(rG$zn~+;PnnV~BS?BJ*nNmMBV(MiHLKKrwFle} zaUy*3MVfKgcscpf(t;6Uw+|jqVpy&^>w25S+FZp`F6vZ5Dnf;riD|6fFIwC#1yx z@jr%b`k?>AD4<tpnrO+lPxVLa6nY;NAhtHCS;D& z$%Bdz4WrQd-$>R&w<^gZ&4FaG%izw28oy-fVDCUhjohyE^$y668a!BMo@1VNOh`DS zD!jljHuQeY8{9y+Xh>LKZX;Z3NQf(b=4+?}XDIYi;_QW8&M14nqmDF2t-$?RWm!jt z@@b=4s;F`4NZE8rZ-2#JVhk{R@n{ymF$zP&zV-@2$$AS2R+0SGv2Ttn&zJ*r!@$-6G{&!3pirD`hYa;s7x50Vto?? zFYg)TST9Qto3da8j@?R;Z-W!k1Vclvy95UjfvnMKK)P{wapHZ7W%hR8t-y#3>CJLR zV~~fDks+z59xW{{b+kSaHu^@sAdR$Zc{&;ZIjHmvSlQ;4LlKY-rha1QsiW!LNYn6+ ziCvZ3fYk{`0`Ui8yLal;DU%PQKY8z?Bi>@u=(6t#brdp$GlLFb^C(A~VyT}T_yC+o z*YPn}VnBVN!TfRj>JX&OLbQ79&|?4pP-O(kHu{yH;VzU2Ugo=PQG3JFO1FHZK%| z!I!L|v|MK*5pr}LLqg9Qz2+F)bY{$mXn|Spl^WhnHM)qbuYbTc%)rsG>56*KQ5xv~ z5rGVG%4winu_0F8D+V#eS;cwwvT6>V%_~o@42PX(&h-p;%ieCD9<&E~>Nb{rZI&NC zGQk1JCsUHx#jLNNcYyiGN4ua$99@i^jgtnlBK?58Q8pmGs~~G>yzZO~2pgM#PT+X? z3xD^lfiG*_?&>bvbR5zP8F1MjE?+N_!#dMUkFpR(uVPR>yw7WTdddh0&idhF(CkDE zqM z$SHJmfO`4158}|a*W^gpt-$6e=yh6Wivk%Bjf~lQgEqH!8{S{l3^)$x6X|4RlubNB zJ)tubUE=q`!n~DH8ZMvKGAcIG z5~2^^L;I<@^9vCUh(hvAF7!G&f>Ukz)5AJe`Rad896_#Wo#A{|9w1J!O*{CiU&!|X z^sCPp$VQN#Plc<)!q#UkZcgxr-_iF2=yx`RAzq%dut<;HZ+}i42YOjhegHDV^)MnB zUJt;{+#g767Gl{OQB9#ie(AXR9nOVMy&e6x(JsO^h0`+Kww5(bz6QgJsJj=WB^0O@rItS06ngY=0sRKSvw^7z&IfJ8*4pH%^_f z^jc36LWe#3^q`H1aA8Q9uRS#EJ80pcG^-&0cK{6~4vj0wo;CbTkwIBdZk#|Ig4W=E zKC{Y`-`!PjVPa#HBPrV4y(Ra=O|$65{-Bf=crRa zr^!Q*Ul^r}rwRf_08SdzojdNh-JEiI;-7o&IgGN=L%aZaqeB8B>LV(=M41uDb-8fiqLn4j^3JSsxmU*2 zY3219Vf|e=f&&oR%ZCYy}Go0ii#EH7kC^5FWT@v5~dRB>A z+2SfnuwuAQ@Iqr=OxT`!uY)?7nO#JBF2$krU#B#AyA~`ej7T0`dD! z5}{t=*U))vBQu&98~sRSztvR z77J<5k{>{R)FtEwg8VEGc0F4zX;}Mk6#a|O>31Wlb}re}gfR8la3i&VtP$*^7rJC? zzj1@HJe(NG3L{q}yt++qNpc|p3b0o_Y%pqiBa2RHRB4pSS}Vi?3_Yr0=c|Bv=J@pQ zE_^rM7+eUKIjWl0zJ4{R3E{$dAP$ruOGy7C!+WCZQ+jA*n0A32I7e@G(cV);7t0Ts zQZK`)P!<99uB<^dc1>PL+%C#YXInkIc%FD^I2qkoTw0U`+p@TEO%D1}1OtOUZ6?;g z8v)WADl&_@5oPQIQcvS4lAQ)bg;79sf`*E^#D)lfba{Cv?^v=lvMGbtEu8GA=o4Ky zY&g?8k_QM@mf^)V%&Ak)E_K(gU9~iFAUo0!be(6t2iQk>==_NZ8`pX6IP&yHDL*e^ zL}=4CY-yHf72qKhlT!@xMA>klyG$7)kG8>s%IoswE2+BPWo1ndH02GM$OCyrj(o=V zA?powz-MP?O@C|@ad^;1*w)v4mp#%$Iv~Z^V@6Io4dNkP=p*4HJFE9On4DxC zT_f*oU!J9kp7-qhn`3X6@G}nk30bh}lo6hj96!%JbTs))10QuH9>RwN1tL_3|K>Uy z%TV8W9$9Bk;cY}9RQ29){~q$zKC&IwX26b+R&3${NWaS9K(91R>ae{IqP(NJ$#2r? zcAFUOGl@F>?sg{m5rOM3K2?*;tE4%{(Tza9>>w-+(ngDJ0#OL0fNTZ%gtP_N8Y%s> ztRKR@UAG(b8QPSi!emR<3&=0vX3Gx*xsdK)OJn(wJ||VljSXNuoUd~$V)tDCvcWZ` zD_YzI^G}>~pb=^yeLSqWuzdOXn@F-4x#}P5w}N$Yl`nuy=3qzsrk{=}AUTlm%e- zE}x#m)B>(IlG%2^^Rdo(dDKr}sLCBCSzvHlix=zJC{U(?p&yKb z#+xFrt|5>T>KLQV=m5_%4Gekn9B}6(tKJ9aD<3p!M&weU{^9_l(?#BS1}J0f8|#s9 z8Y%w(^ac`}-e{yK(7(g^tAi08ZlM>wIqCul4rP`xZx@gNc9~;s&hnBT$p$c_8^;Y< zqH{RZ*agTyDAXvmr8Im62hurd; zyzV4JjkLe6=%k11({K_1d}4GwL!TCD&r|lZIZ_XTm*p#-3@8{IJ3`$_<$!cj_|XhqwZ= zh!G%fJcG=z@vX{*aQ1iwNtCXv_&N>34+kKx%+gt0SU5OIS+HJ{a^VHj_+0F?$n@a; z?rlxG)_=X1-h4~5K0|Yt1lbUN9{${GcI7X;VOOrN?aLF_wGPDrG=tvbdP6zfc%lFp z;dA@r>9BUdGM!x4b3V(-XY?>T1IL{E_->B9rWyO_`N%@C%-Hiptzoy4Ik0r=eK$*f z*fMMy2QooU-j3EIPs`8Kswqt5XZAt=vBTi;p!Pd-W);o5CIy*=QX{R%&xNXGs^6%; zn>T}Gm3n;y>qz7(g3=dz!Joeq1;r(U`QcG8Ok3kcfT4Gah9(drgcGL1#2~}oxTV83 zF`6ObIYziJNTDYd1M7|pgODLdMY<58>>s26FfO^YD4TO(fvktZB;9ULavrp=N zIES85=Kv&<`Vl|NC-;Z=0Qp6qxSwlj9YW?R^`<)wLU<_;KG~+eRa-=d!T31uz~Ws| z8@2|m11fU>MkUFIIr23|e|Y)1+C+_`R;825b>Hg%Pq_ZZ2dMD_J@l!U^F9czoGm7lr33`4NHaV~2tq z!l!p`u}RCK-=shXZPN<{^|y>;OXBqUK#7soB(0h^;^QgK`O|t$e;hcTIk>*jC9l70 zPu_5MO)hDZ`s`v)v)db?`HSsXOcd@O-jlog_T{t&x3kkF#kzR+<$;lXxxH7POO7!m zz}IrjjpvOt*e@fWCeI}OP9Cs_B5!-ap8SbQNj|>;LEx1U>KB+S`Ojz!fixDF4{;7nd1x(-)cZ%om9E=Kqa7Y!8t*cl}evATt%EK$`lzRZ~qSJGB+cZDg#~uNl!$R zqUiusmo+^zV>6~pq>Vhg!OZT72IvHGA@9f_DPB;EEtT@kg z*%w0?t)eb5LX0d3i;)fL82OLmqS3|)!Ewfs!-3=(4#G);u!55bu^3icLV>5rOh71# zygHkHrx^q*z?@!9+xi-#GResdrdNVK6EvV$N|oghj0#^K;Htmj_zT^WvtFcB1<;iu{EKZr)rhX z70$ZuT2-(X-@)&=4E#Hen14r>m@)2)5B?c+ zPRe9|&u6R5p59Zc~s|$o>v5!2H{xwCgJ9R zJoCIYi0WX5_i3O!47anWHcTtTXyeqzQ?{OOApNQX2aU0hI`khnIyfX#)3ZiETca*l zo|w^!9=7hF<U|P&LhiMo{X5J z^M~v?e9EWqQkDmL)NxW*R@M%Ux)3gMZ3f~mMm(D%CRl`d=Ij|8i6ws0-zt2{2VD;J zEk+<($b_)6>Kb|FJoge-EHmN`qgxPdjyN70x#GpU+RhLMqq0S&Y7!1)60o^POb|N( zbcgrPk)M^7B@N&8hEX`uPu;}ff?(37P{%p%RTqE4+hD%?kp=fV!uP{R9Zs4W&rv`9 z6&rMrHP+t{Y%?N+{<6y@@5~}UtYM-Y12GR%4Sky6Nx@~sXDd^KtVkF7g|Ff^w$aI@ z+?Wy-*1`PgS8U2-m)w@(tsxh36FxzZRUL;;7*@8NXubaKn!M?r-Ihd)bXoee4yaYJ zbJM4GgHY^|Mu=HQ!)KtQtZYE`u1CPi3CJ)i?4RwgIL4Vg)agw#P zxUM#Fva=&uIK)gBtZr#TRaN3s@BpPU&XRpK!qr>pH)c+yi)DkN`CQlJL;{>T8Z8?8 zus#de2$nYfygZk!O!xO8NqLSyqR1BEVS%{78_#3FaN2N)w$zBx%Z2gfWtH^Y(Xi1l zRy18CI&M&Jb&yvYrnr%3>IZo>WmHk{qG!q@c9^mdmtmf4UHHQ4{-i8$NSV&n4BVSh)JFnxV$1!FjWsO;Uso5eD?|^m zV_Bel&?#(h-daR-*mOh54ELie!4U`OR~8|Of&JdvJiKvDJBSI^^z@ABJcN(@Qg*CO zYGum+?^}vH5Mn?i&<)awT$_2U}BTY5w|k+g6;8 z1JdhtBpbYQ3)yoY(*v8`dsNKM`Q|0q(3O?LcGTNRuXl8MhHdA0hI-?Aj9)>0)|${n zudTW#PEZyIBWq-rce5d9I*ShL5aH#AiH)Itgm>=QNSOK+5JpA_ktv-FofxfZ2J%MI z-L5r{E4F>;D&?P+3BPe}u{T_LPv%DUWp=pvF{i}j z)L6IlmG|VU^Ih`F+xFzOcUEO#qg$Sw&n7jbmoyxtaj+&o{%yN5bG0IW^FQ^-e_1Ta zrCQxtZc4BeQ@3x+d(JA+`(VHPypo`cyOtBVwyodS%5v(=w!A=k^2ltreByUY@<+)n*lIafl^?iE8qo@tgLlf4gz;!dV$1|78;(VLi7O2? z=N1U}^q!_21sckD9?x!8n*3Zm z{fWG$3D3yrxNItk=!BC=14z2k@+Rz7WFbaf#@GV?giI4Yb_{)s5AQ8!Enu)8JkuEv z9z+!zi|z$L1h8ApfX?GIBXc^I_(UH#LPrx0KII6=i-IGBhU?%|QJN z@}8NQwrP>{@CR}Oi9tWJ>QEXZqhX}+Fwng~n#d!+n-ML*Jsj!0p$8))V=B*H4bP|P zGCh!d;OtYU7=_r{;#~4j0u9c#XJ<5*v|v*hE$%cRaZK|iP3f(|(;$D;Q}hKLA(LoUR~=iO8L@_&5CwtW5lwU)=% z-CvcjyKmRNzxoc^FN;daR#na`SvtRzO|&#-b9BMCylzLnak4DG{=tgupQ*}Q^muq? zPad978@b&r>n+w~z3{djd7~1Sg^NA%+*+3`CfUAwXix4Q*_AgwSe22Liu~eJbwN%| z?8x8y)^&N)c(;7?w}<6Nzfh9byl7J%=CG?R zmcRa%Z5g`KC;#qaJ@N-?>d=R;f900E>6F%GH36RMeVM+kCa+Yv=;H~LnBGy7msHB~ z%jfkZWzs;Jh}Zq?G)Phwfd-U+<2ohx+s)1Vb%bH8CyW4| zlzjxTI;emk+D+wyQ)dFU|Eh z$uA9H@xrFZfyR%Qb>g)8)Ei&)i@fN8#Mz}cl*ZV5EJ$PL8^Gg&o?e{ZkoPbpDmWs@ zoZTJ0H!@#7C^K@5%j7Up*Yp`Szv$EpiL+2r=cljKXX^*rb>48CC`V+19703Y-|xG8 z(FoJ<5-#64LN91I+_7*28RSPe;XaQy%8n)?8;PrtGU=D+N!iXyl zHEWrI1AG*Osilz0kPkj$@?Df<0rE$EMc0rq=|n&B9AMHOU-PMgb;p)b7M#H_GUvwA z?HET|_sp<1(Pu!~h=Vc;pzH`Po~GC?SoajBA_v%%3QZ$PBE7+x^R#ycw$YE)_4HQW zVVYSpkZ_`k~}&PLdQ%1_s6nB=FP~6iaV(r?dvH7D*%GU#vZ&AD((cJ zBLaCA=`eyAumKS1$pX5;;dP%5gVs|?$$NMvfak*KOMD(sN9;ONn^o>{ClU3Yk#af| z*EcrRzOi{pa?bU`_0Wpplrh&wI4o0-*_K3$I4pUe0J$0Y_F>)gGQUwy6y*gq0^R5J&L@8u<66mnKYq1a z?pJc8E|feyUy`5w<38D`5n3H)DmbN|PA+QlXWv?r*`7VQ|AAe3)m=5YYi3{WI9rv6 z?%0*rKB#1GtSrC#bayV%y0qLS58t~ZFS>6>-uBXM`L37w@wNw*d?^W>Qd4vBiGKOD zD<1JTy<%N{^z}P3vd}00^j8Mt=k$3^jeO(FcjUI!Uiq;neCNAR25)-tt~^vL$uD2u zlfU}yTk_2_-SVN|9F%|X<&s=Z1dJ|x)5E(m`fNr1r{6BiZ+@kuN4GrulA82A*(*Qz zp>VECUjK@k>^{+>=^-`g5=v|e?PsItOm1PcB@J8eWFugqJ>$?eFA%L82;z(7mxf;T z%;@D{>&UY{VBe1e;>?=P$$8|1{Fo(F2dUwHY__IZ|B?*uqn<#rTFI~DBrE+g;&uU3 zjHe80fMyt>jyRh|fiYpq7CQ)KHO4+NhU4bj5YwZU<*>%6D`zpV?tnF5G$J8vK5=4b z2)EU-pr@X~gK)8H7E@txW<38sLUJphe_&8{B4>hLkw6>(vO$J4LTM-9FpA62(WNA- zbfP4ycc4#t^SQla0CLm zO4}W5X;F|KMx$7x$*2{!&cn?g@u4h39iaYE#(AJU=vMpFY`47_^?cRiXQl%Le0hl% zFgMyO>M&(rq&^If4BK?}W;T!dM|k85J#Ln_fcP0eS|Bdz5$FJ}qh{1C95D2dshQ&w zlv|f|@D*D#+de3dQ>W%My*TdZjc*8&SKe75pG+gf!C}oqpOyigpsL~K6Qt|8SE*(; zB>NgfiD`Z7>%>*paaTR+J6{ez%Xvl;ag=!Bau$$zSC{WzihW{Kv3VNXM_&5y!HOX0 zwAlOwUmudSZ9;4W;ZqirjoE`!HTi)D_TAjPka&OzVk-lL90QyWphC{gKcK;O*Su@JRF!gPfecc=)SD(ls&vA@+(5Jj^-g8 z5sYH_!ZL?tMulDJ8%)j=1DT;$zTD$ec49{>fVCzo>TH40G1gACo?nT4GGvqq8gZPj z^y1n8b|+-o0V5CBu3ohfFQ$M{U;~5wGB!RcGqW=?l1#gyux}c)LehW-&={tt4sEM* zP2#5fqWutUq711B(ia_~%6?>>4?FFXehToyqL&RrG=n}!!B^tf>6nuY9*&S@uP z3J90o%5dK3bzbFhML^?6S;k23a#$3Q z-#xamSJ{!T_y8eI{0@!fQNihKT|XF&A5{`NJ3Fm;VcNZ&I)D=n|BMfi z<;VciMVgrfkcK;+u2&(P0Gp+wHe*}m+YF45ZEmict)H2NcxI>LsKA%YbW95vskHpi zEo-9&27TR0+wdhA)ECA$cbLOA35 zGNw-Lyb_c1NfvD+!ZOn>8x)6IB}dVE{n_>wL8##e31JM7qzIgPSBu_JHR^DiEA=eu0$l38tnp8NEG{QtkuEtR`> zTjuV%TduHzOc4;IYDvjg4y~)2czQ-(M6xgE{ZYq7l9 znDVo2PW7<+_r0?1(+23zy3wBXrxReK4n_sy(HwwL zi=AA0ddcTeflL`SMSko)h)i&rI3LHu4q|aU5L=uqNY+W!9pZ~ew5S{8KRDP$L2o(Br^U&30AV2iux1PU4rwFb zgol1%`0J#Ky3o1~5C(NBo>G|&Z)XS}JHj+Z*0P23&43NW)?=q#uRA0EI$=AU2I_wv z-1k9u>LXbJq1`Ogrl{~Zz*E=MzUpHfTmytcP##+HU5|P_((*{Y1!(Z{x8AlZuezgV z1nd3(wIZu34jw_5&3V@E-u@*IDbR7NyVEuKp4<1-{}bukDoKC$zMSb3IWw^*-}0am zp1byB=ea)lXU}Rx`uy8pyDES0^)(rJyhr}{K!IhlX6*^T>%rU* zkPg!8KtnF#tnNrdT5qgqa0Klol4|Z4`--hEu0b zCs&RR97)j5l!+Lm-A1`IT{K>dxM8d-JtY}bqfcXjlRPpqWDZA8*>(&xYH6nj$CkM3 z@U=4SI+R6F-P}_G)ni7YVI*o#E0G?1Rc#=4R8KamcI<9w+3)H(ZIJo2k+7fws?ps& zTDdiu>{{3}&?7YspLGp+>z3LD^1^WC!Ht4{0`4_Kr@zDZ`Z=$=;Xt?OJiuY(lU`FA zbL7quM$C9N2F`SF9O>m|IKEQ^i@1c$zbqSrJn}~5xF7O|>~P+)8oB`VHSQH{ zhV4yFU(#D$1dPZ~S4l&&QK=Ydz0MnzBE1j-!VWS!3c)D?UN$^7Heu_J=pny;ZN5R~ z*REZ$bda9ofRU|BmoCbtP2;Ya{1`bK)HWT`K{g>+e8-<^R%Yk`~>pK^C4D`BrxB z`-Xh$ORCa+tw(<8)1-~3_T<4cdorj|uQwD|mpt@}wS#B>m$z=pD<-<+i=P^jzw}Fe z@>>O}$Xmu$5vtvCG1Xj7P1R&v9g54z2-RcH_RF99PyOK&~F*|!k9hlBcJL(-)Cu67X0F*0b z8LAAm+_fl;v583|CQQeR5m)FK^aA3v(c_b~fk06N!`g0oi-7d?^!W5MAF+Y8Qx@H} zwuVN;dVx9!7By4G?0^@*oPWTMc|lp(n4ncJu<5a;H7I%!lm@zuWbIoICgs;yfCm*q z^&Z=7Qv~%(x$Mr^-j>Uw)Wt}b-#gehB)i&hGxdnPQIV-ktjl68N!qz<7iZ3#mJ1gy zCco-9O8!B3CVb}Ua16F7Prn@Y+_|$#&?e0h=+rbKNE_+ODtF`=_vSJ>0-@~`s1Hm7 zBwTv#kvZvM?NtERAX(S0UhY`kpd2Xza|ROixk}gjL9V6JbA~<{YtUBQ< z4J6nb4a(iai1o*#^j^0-K&UA1y!_+L7a3j21GW|ns9hos;zP&ULEcn4>LzIm>7#C) zKmVN7QR2bDPv?s|4RPZ$06Q@`HEA7JdBCnRhaR%ls*?=I9ml`u6usL=H`ys>{@MC>qK1#j|<`<+(2@f4t4p`W&556a-ky#ooiPQ z0%Au8_z0QGODh`nj?FwaH|QbudwsV}oTB7`2#mZ93=C;n7F3qSiSf+Lj4UkhPAu9K z4#ICtR~n%n+P3*h2sc#*SzFo{K_FGs?-+;MK$k%{pJfV`mWt`=sLX2~Yf@Hu_VzII z!ZK~=GzPmT$RZ0kx?H?xYG3}$tG49-{ed1?$?a$Ik+k5nhA^#jy%z6on>gEg9a~68 zCTq$#XLdCwEjI%!KcIdG(rGqGzsBdX;CWHmDNGJ}EBW8Gx}X0Xq#(!un}{ADANLdL z&p|$HCR7qOZ4_x@rc%1D-83ZW^7VrIW^?dw(=yEiGUOIpE2Y25f1@59Ml3&I&s5QO z_p>-KDa+MCS*?0!AUeCV%j_=R8>_xJ&*xxz`i%O{7tDrxxnQU2>A4}(1)VtfR4SY! zdR2C{fjE$-W|kfnt9Vgk(+U&}&J9z9Z~{YsdHRG?N<&UMQ1*0>Xb~{ghd0}AZZu9` z>J(5k!lm(Isv~PR$QxwWM`H%8&>793F*z<-0Jea?)YGtd8X1z;*s&%qcC%_V9m6Yh zUCCO(hGW%EQG{J3C6(oFsc0S{-#z_3qS_-pn!ZX&^Iaa0fu14hEA`7@Wl&V3WRR`P zyYxB@hE^y$w2awVGK_edAylCJL58iTmE;2_gEC52Zr8oyd9NcpQ~X&B6lF&qNe2!R zBTkTi(nq}u*NzJE1G&Z#%nFB5Cv-Ld2h9v&hrkYeyg-^YmgpnSrW4V6TBfI`lJyep zjVNV-sjH=mI(a=1e>LPv+Bp~L?vYS>G>D z2M%T@T)%$ZzLUR@FXBeVHpNTZk*tOfCmlPVAqs;{L)A1e>e*@}h=uRG1Fu~>LHOhc zogj^^WY7KNmHct=OgO0H{$9IoBZi~}dj{F!8g{G`QwAXmGg^;mV;D`OzRzFJCr{*$ zdm-<1vc%oK6LDZ5~=v0qG4M<%p&z9~ih{ur!n=Dzr~1g$_FqCKkVf@k9A%m%6t$bIM_@R1N~pZ&ZgK$S5&{(wzhD5BoaUV@ak6D;tgS<964>kOqHnezQl9vTbyP?LlT- z^Y{4fY1@Z`>T#vfYP#SJAz4gM!TG`{+eQJ}Kvo!q=)rMk`U4K5I@CSdK(^7KVc6NF z2qW*Aqk^Kuhcg;LgT$0P8aAG9htvTYU3vy^Y!K>6g7BICgz@yo)u+OkBU`5DmxZmo zlZ&_P`?{*5fQFipq?lw8H{0#vNYxdGU6@9!F$w3;yZAs?9F1iWJB;b?6^*;M(kJ7C zlhUswYOpdSm9C0(@0FEcIce%qeZf$K=V**^y6MfWY9&}%W*hx=P49-44UH`_Lv}dA ztnVRdY*qk4C0=%T<7JcW>HuI3P;dxhSn>*h_(RWZ49*9c6#??5kw10nlvefudHU&R zEdTVrqpNg6FcNu`JOZE2w6C(vZorW*G9Vr31XBWV{yG5~TFMll+ms_jl65!8nYs+= zq8>C`htm$wFZ>KYG(7#^UG9?IEtMaBq{eH63*)Vy$Y*2C$u|0C9To?5;4pk-&UYn^ zL4N#j9d|v~=*{Wz2El*(c^omyq_KWFum|WTU(ih{J*Zg3jU#^$yxtFup89L4?- z7WNAnkT$|dr=L-D9N3_8iXZRjulLhMb_I9)F!C6n~Yk*tXOOiFJPI$Xy62wFc z``RqcAe0mNqx|??R#Hnj;=s|#5Xcl^kv8PR2n;xm?lU6ecEZ>3(MeN;xKZ~Z@oXf* zbM3(8QI|>RKtR7CzwF+}Z+hpkZ^)A+1;{jAwzPfh-b?*!hj4}-{&*zb(?tDqd6EW8 zYH}_uX5yo~NFzm<-p#TZLL7wMh^CVPRVOZXh&Smga^$yMIi+i~woc z`3E@1HM>m_^5XgN*I{r^J*;dnsvG16!ZTU&v#>~m9^oBaJW84Ii9I@yUlitz2690L z2eQ+2Fd`Mh@|=R-@3zt|>tXAx_?II>{2yyqO~Lhqejqx^0nBgp$ih~bqD}|ko|7wU zf7~vS7UK3u8*xjJEE;4Ej3A%E!5HWrkUk|=ma#4+O#0nMZ%lcL0po_!)TaozK=YKA z^W0CPO=G{MMzUyp$QQ!M^X)b&#|7!8QN^g!=(Bx2>84S}al_doPu}Crkr;#mpl6tH z(s{xNa1a;bBn>onY-LMYLIpwQb@1J%IPxqGcSx5zSAAAyoa4o@^dwpXm6REasmUM4 zfQGiNGb@@tNR%JQfvDZlsOmX)bUCunuZ{q7=;o)Jz-& z;PvaUVK^+$)}S2Nm;&OB%#j&oPd%&cRgD-QbySOR(ThA?LiRXCAzUA^?hMHaQ+mlC z;Zyf0cjS)zd13Q_I^NvZj&s!vxEnaNF^q6kUAI{O#|A6=Y_Oun7*ffmH4rTjoGy+m z;FLnd(j==6nmTyJAYCC3DP5v)<@Fg;L>Wo~ya!Kxg;KkCV`Po!^hOFW=BCb|doqI;d zCyjLSIsvBI6?N1D&_!Nl(#~jBuzhvC4r#6HH{iw0*lJ!73L&C1g*Jq?gS4aP{ex-; z^fzH{E5Rwc^*`Wwt->nXtY=7sAEbvPPX(wC*nz%2@9<35%RG1)C7Dejt8Hk7qGk8} zuiTW!E@`~`HFZ4IhHJgs*Ru1?ryS(hY?!5?o)C02CJN{_gpE;ulUGinY2U35A7r-x zxlo1wQtJlg2r+ywanO|IFgBu+j^S4gkPpb5xt&_B~ppn+t`B8K^w`e{VV;PSh^ z!3b7*4KR1s!{R>5*okVFtZCaF>^f<Vi5$+UQhz`(;F( zITQfrn*27ChY^h)-Y;^vcb(B9;t0X{4g~NhXS(I&Zrh6 zV<5fms}bI~1ayjIF%lWd0eLWD<+63#!DbWac(7CGXeWRXW?f$>0xJ*o$TQ1CPqCXN z*O4!}3y=qTLET6LCz`Tqj}($ecRq1EnKl-uhk8!gT4zNfqfP=D3=R9HF>9-bH>)@< z2p_p1L-Z$12Ta$kB5;}SshqOgD$p%SUd z=~-F0eoYqUS@-LX0aNo# z>m<|i)~9U^1#M1#=fz?8tPIMhRoRjj|I9-mT67|!yUpYo>ars*Ot)+8peV_DNv z%yzg1Nmc+obU3>-7#J$zWWia{h8%-MFDp(~P&&fM%4`gOzg3qS=^mRZSEPY}a3Bkq zEJD9kfUIylG3<=^u&59@l*&GG1o@$nC+*0FwCjic=DqG)!-S=Ho`SwMqCJ_A{}Cm%D=SM@HW&ix6l)pi zp{5LQI4J|}z-%tiEa%;^{UR9Yzd~12)r$on+EX-HU;P1xO!G z4FsBeA1;p!Bt|w1X7^YKo`{C^fngM@6=I@=z9Ap#Pz-dHd)l)fB-p1lVJl-`H>t0r z`=mhFqZ$Ww_$VXzY&3E2z4zKC1Lz_8lh&yKbd);C3*5Q4U7*|4t7i6`GQq)2%Q6ed zH*}~R1;`Ga^bVm$=OGP7I0py&ms=rvPD?lprr_q6A>gPS?= zE`MyHZFXTeoMxNer}t6su#>Ii1l$LJEU5pbvO;crh64~zp4~kd{jp5kJfJ&at9;T* zT_7)f^4kc2rjsQPj*Rr`eMSOQ#_h_FXRuMknFa(TuO0;X0W0I11oDckn#s@7nIjWy z7&?ns`EKfp3^N{xiR#t0l3doqhMVeIpM%k$C{Cs76JK1%kQ?#}I@b=u)Whb_V#nCE zuyuPzSdgEh`A!1|D@>&eb~X)c()7%vQ^3ysz?#)PgE{fI{NEXut%{Rrr(@*pBNRmeA zPA~EM{bnciz!~)o6uQg?Q%6Z3*O+RE1MBH6o@mM^gs=Y_V9vA3J*)gEj%LpBjY3Gb zjm{)##b&VH2{}SeNhdJ!s^{2LAxsbSLj%xrjq)p&FOTnN@}rIObQFv0BtYDj2h+JG z@*{~kYDL=-j|dV*W}XvV->AqnJpeL~^*W8PLu1x8Be+@*o;M^b0B5~_aQI+L$5tRy zI9M3BxS`IFNhTm#IFt+-5?`Lv2}VYTWp~FN*5?b5Ng!PuINUb3l->cA5$_S>H)Tw( zXqdjodv+imJQJLQy7S6wAco<+*r>ekFt?x zrf)JDgp*8q10jXHf55r6~`7oAJZoN^q?yYIeR-tmri$PfPD z56ZWE%eTmV_uXg2l8!Jw3Fr3PZOk5eV;vbMlPOzMiv*YTU{PjQvCR1kE+fM%WZewD~~<; z8QD?0x1}WP-0gSC`DebWX*z5Jj{#YvtEp;t{lji>*mC%?D-v!!Z$N_`T-K>NWGLG>?o{rSlpiCRhOwyq#Tk@~*G13&380EMJx;8l7xKvS`U~InWT` z%oWAf&aOEfG2BUM4P+7=yAh3(k*zGy@Pk%8b+g6kP?aDQKXVm=sb#(-p z+rM?-5X5kTRwpqyTVc8uNXwe%F@h%z8>-g|VaDR1^5uikol^I{O%rJbWWWX)IJc|? zV0(K;gwV;L2gJve8?Tg}|Cm5=E|v?|yb>lSB5=+R9~lZ^`X_PuCy4655xp&5n3_WZ z2oDDz!b})<-F26}NEcGV4q_KBJZIk-*$ksVfbSTW;Ha`79%GF|$cRSRX^_qdjC-6< zz(`ac0dIzwU;!NSG{^_jRB`6gb&=6kM%W;Z?5F+_u5c6bb&}>CT+7^gEfHdRG z*;*2Huwud#^dubNT(!1q1Q#7l0}^?o2o~r`Y-_7&Wv5S{mhbqE@37G;$W>^d00b-? zlpUXpX1(ogZb2d#Ga`oJE&o$UxMq!?N_Gy`#p0Q4zQ)kY})hibpbSDeds&5+~?4j*Kt9I(a z7r)$&+wQ!_&UYHSO5l3R>>k|yD0VZaC<|V1eQh;6k`eFb)~1bWBS+q)ckL?c*%qwB za6@f4_c|FHv(8dpa84b9_|ML%-VF^HA%&oa(WU@?x8sP`v<#T4xwC!vRRV!vd6<4Q zL#yk6q_vT}rij~7n{w4UdILJ=1eRazCdNCHcwZFdl-36)&E63ljt*tF<6vaSM~5iu zBFNj^N%TM#CGIV{-5_~qn0()vhm=q({%v3}=&1Ry7z@IVZ3dh#4MMm6Xa ztiyM~q7r1un+o5`(dh&zbDV$DQgj{A&Mq4gWKY{k15$>Q%l7a%$2j+6;}b?UOg}a5 zAip$X*#hKbjRVF)59((49wR&#KOa8*c@GCik&&n(y_Y->F=Cq9#Q3D#cH3a|y!5;KYDv@yu>@PoHyVkqruPn0Y2WZH@Dcq7}iOI>u>&h0bLmOmocZ zF^3plx^&rg8p8;OvWgM=Ei`Jp2dl(;u_`4y0_?@s+4i~+0?`V#2_5C-syNkjZs2r7 zrpOQXAah0txxTcxoJmw6Sd5gVp+%tQd#y3xdndpPhC~yyMub>f#W6KK zb#T1oB`=b{_Sb&INLEZRV_;{0`?r7FrVf7WV;?hpB3x|v5B$In$Rm$DBHPJmI?h5n zj%@%=EX1W5m~OUPV5%Sp4okZ-<$fF+>>wkUlau4>;7^#Wc-b;C+R{3Cxa& z7UyMPXjnUpPRJLHc|^EF=GpE+swv$@lm-hu@ogDS)5*l;3Nr z-ORq}wYG?swoT>$tIoI5)5Aa}s=PUI+2~L5JrI>!9TZ85?=%Hv806PZ2Hi#_5jBw? zbu-uma4U;25Y9?B$ff5$ARtZWBOT_IoL6HrxMD)w#wq(;^E#(h-DJWm){sS z8X10OEe5CjX*k%4iL7%lq7ggzu}lbuX;DQ8z;QsFkYXA$8q~Ov&4V^;syDoErQjwB z3c_XTB+d$p%`sT+%y!$Tjn6B`C^XYK&eLf6eWac5evXEjd;E7kJPZyT0vv@PW75(r zey)*D?njStmPrGSKH*S@FdjJ14DR6Q$G~B<-MF^5jEpk^#E2JUAzVj~;^}0_i#Xlz z9d2})wh38+0AlEZ4*BLBjYOFAb3mLRhs{RH2#4|_Zr0ZIYn|pC=`rV1>vs~BK|@Yt z=b)_z_&VJcuKGP8eeD2J|B%V@>cTbI+TF5IgDow)?cHsAsr<;;hC(~K$+7E>g%ZEeY> z3ob7y5ns4&^1Soz`)qyH(@#CAj{n%9>4GcEOLFGi?MBMjGzJHNHCk7cl<}^u>Dkk= zaQ&*x>%k}sn`H1zCxObMlh~I0-AI#~l#mC$AEizOuc$W`dr@C~HGuDQM;3N||(HzVlWQ-8^Ej7AxR!_QOx@wxi< zR$?Nr&2&CSZ`YQ^rlv3n-)(?_-^o|XhAC9s#J__Jc}T*t@YLSZjvS?ULm2wMju3sT zG~8ln$U`ew9`%pPEDSKTTR*rvSAP6XANGJAwEu3iFP2|3f|Uk73^8IS z(rp=jA3~&VbB*syi|c0dpI2+L0&vR6a5IvH^OAOy>oPf<*;c@DV>@`97;UZ_;>PLV z1!Z}?j<-5oF0oAb?!VCs6vvFJI#XF>1t*T}frAd=3_vt|#KRZM@<}}0gHv%MWW{@u ze0%u(O&rpo(WF5tLds%rKQ9FZG`i%O?eZDXg6JW;K%DYWue97#zmL;2ycu=$5U7*G z!=u(i8!EMb)`?ph#2fCV-f+-U3vt5P^SrH@gFsO-zdM#EQ)#FlkSfZZ`*0!x*}<`6 zL;|uFoDSl{p~0Ckf@67C0;GCDT$~GO3cz_`L3*%1;RwL73Zq`}`7Drr94B-e{pEAF z4I4(%AY9g~R#x)StAf$r*1OvVMkYrrj3IT}AQ9<2r+)_^3#KnZju5Yf}`~EgIH}p`_y0sxYn$K-@sI^8*P4AU%rlOW?!;c;%3FTgu^Wd;ewPJTx z$N(>NZ>&j0@5Q-X(MDufm|*Sn^|Q-IzRf+ zkIFy%hyPGM{pn9zen0uiPueuW0Eiar`@mz5J*H&dx64gCutmV82dp)1_AF~~A+^+D zdO#te?B>T>8uXhN^bSCh67tlm^?iJ7-15k|#>1!AhWOUjlrSqnWW;x4L&>P>ATI}} z>{)w8UCXmQG0B1mG3D&+ZFk92Pd;w%D5G$K+QugU82 zqTGJhz54sKog-b4woW6+hPs`uw~gRDeg%5pybk99;>$=nYcRqrlmL(WFo_9i;-K@v zJ7Op^pTdi+qCNA@t4*_S!N=elHlFE0Oy#7LBIK720f;^1nhsjhM)_`Su^~rZ{x}~W z&Cu$)-l2IaO0vHH75<(q||J9aJTD7J|;NF*aKEg;?IBR_nRAU~I3 z4%*DijRt8)|JZ=Sease!V!Z$kx`!Rhn|F})sqBhx#eY=b-~{ESL2LyK5Cnl|jylqYeCU;S$I9O) zf+I#|1pRF)3;4ebP8Au~ka_X$d6#{GwIOIQ!YH^+3ee%P)23`_P zK8S~VklDh*qJ>Y`j64v27?olD6zlf5M!M3*hWL;h*Mft|z1&AQ9F%1|O)#7f2aYhr zBX%Y@-wedd{oF_R1L{bF^xNf|!Lp(UgsW8cX@!IrZrt9W4<^U znR@TIJ8FIXzPSW)hg7k?X<}+Z`jo`)sl&Lr3+dUgk)zsvRS(FM>OIb+CXS74Y<_(r zcbn6$2k%besM^RawxNYMIFjTb<_#F3%jiO zw;pZU@{n;yKrx@|-)=c9lTM8abSzqH*~v!dY=PkcB?6W1pvCRIMF)6um?_<4wxmo=n$ zTQ)a%EeWIl?i)~E9K3JL>b2H!>NR@P*Uu=j@6^k>UdX)J4LUuV#@l$f{P4r{r=2?t zt`{X)Y<%&BgWUzubDsB*Ep+{1pY|2&qI!v~vv#cwXdp(k^lM!2V=xlQwGf}fQ{u8p zerG?PT<<)Zh1+hNIi zoqn_J$S99!(Xo~8IHgV_JIEcMp$yY_UXv`Gkv=6m?e6M?ELQMFJdejro5Q*JUDC(c^c#$8PM1Oc0K@yi_wta zP@fcNz`R0qA0mf&AdfhlAumTkaLlet(Q(7jZ5un>yKy6#8EA^3= zKL@8P4}Q(-0A-hVFAfR^qX;;mPdxFY{$_+^(})#mvKMfxgGeU>zcDHhE*utiF(c7T zs|iyFsW(jPqwHM{jUYWKuL!_>MvXG)ii7?X&IDZ@~X~04t}o^ z7@RLgO?desBk6>Xjx4P#%chc`s@fnUH5Pfs0pjn+freqDRS8Chlx*uKjR;_Iu{&IP zR`0$0uDj*C-}gNYzbOCYpZpUePHCJwch1&l1yF|6X9&w5{^1|$nZqv? z<{oSnbq^ime)I;CkKR!)(Iuu;cCo%e@57b{{Xj(MPj7FJ%9gcR+nT?9+wh29W47ca zf9woLdxtYk=K-6LFiPtr%j*oI8(I3D2OI_Tka|J7+X3+s7y8C0_9oUn^2a^cV}K3f zCHvTHbe{Um^`ns1qM*()Lg>3^0%RT^jnLZ_htC(m$mpnzzNV)&125{laM7QN?5ka5 zOH;G%C-#+Izd>`2+Gmwt^2(6g-3N=Q+Nc5TXP5**5181=@}1 zT96~6vra{o-Sba!B)k1I8^PM`w>*PtjxPeJhuBjaXfRz>x$AlCG)LH8{vZ`mdYmn6&U7L4vv}GGuz8@Q5t4-S$mSQ20SFcOf_y=;*pU)& zo%o~_m^dKU{o8y9$U+_tfYx4eVf}j>OLKA zSL0?xicJF!4n4Q6#44cuoOT9XY7<6>M`fMqhe_S)*7Dv{J>e7Q>s#LOt@4^zzvfVR zkhkCXjo*-e{Ez>!eC~6f%e3L2|NQ6W{qKK&^2h)->?~>|S$F zx$gSnjth_8ax*8T5*D(2i9qeMUH3&;2FOtkFWh&||K}WU3P& ze!@n7A%;akzK|#N9vu#!F~V=+!>LCmoytQqKo%gnVI->+eB+1awkZpPrVBoO zozbPkXdX9f$GD?N+f)~Ak^Hbum7en+U+bmqsOgus4V#;gYpvHiRqMpSX?6na@1^89 zrUP3CRZr!Be7Wt$d9x^*qG+4&avRC=96_>x>5+Ys4?hW;Ms;lL&Zsj zBq1N}O&<^`(^-upOfdvJXV2(wJ7?$7cfM+xNEe^U;W=tN_?t%Fy>VDYkF0R`n+Ex# zK3d{a_lF}heEnZT26{(UUExWUtcA~P2HG~ek9hRI$q(YdeSC0Vlm`B`f~BL)gTJlN zZn$L2?4M@DxDfim`P8wrn-Hu3%`YRS_-7nx@UARdZ>6B^U@9=PKf;~Cf1xdCEXU_L z$pZAOgLKbzD++PebO8&z{ZmFYeslI&gW^<=}Dw@i`xz;Tn!;7En;O z4G%<a_^Ny{rtQ6e2hy2H)*W{22}a!H3L;G7jgD0-R3D1E;;B z`KAf0?rWM;BBgQ4B2tWERd*T2N=~Ez!D5tZsBg&9$;-|cmX5v#=IA*+9@wI`Pj!5G zebGnK6gY^zr9QcB@{~My+Y9A8zT-P(VrKH7JkSf)AN|XJ`7h;%fB1*xnP;BK)m4bi z_kaKQ+bC2(xEBJ(S{rtAMHi6*4(eWwQK&w3Jg`^&HieIMnLMXzqlB5BT^i0kSzcOF zho5)7t=d6+=n&+Iww4h>bRn;CJP8mN`9WR~#Vp{YkI@*VeF4`C1D(raX@-b4V5hgclO0N9Kyy1=5I(itSWB2(Nez#8zVS(YKI4 zCzI(lUmL2AVY3@LSN2r5tE#&`^2G=&4j*eI>0mJOrG$)pV~5#%C5&1I6m9=rc6v*` zn2;>QRUce&=h#H3NhkGo#NU&HjwvHYRJsJY#i{3gx>jQt;duAMA}^fVmxoU6%ja1u zreV^-;_ZMQ`bpU4Boim8)I@SFjx&T8LkNUn&(q#%sBkUdiX`q5JoHq>O%6p z87#f2gZR-q8?kH12jS{HdMxi~d!;1HM*l(&f`V(*cj{plu&3NZokM=uV3VDGx6#z3 zjlgf9efL;J`ucfaCF|PrNmkd~-08z-6<#OeLq!3xXgLn+`2v(CPq|`DO-*b5d^_#D z4M!MF;QdPO`1q&JCSk*H7alUQ`5RIbS7OBbx3g}N5LXPpaxps}@b z*fb)2fba?8@6AB^A$~sEwkI1q>$0k6-lUgsKyB!K10tt-RhV90)WQGo zAO3^`h|1KXS(hI z>@{{F>m6eOIO>eTHoNB}K;7nDZUFlc5NsF+Q`vYo*N&3Pu*nOc4NC8bn73U%b(U*% zCb1hNWCRst z(<0xc1R@SfCf*em-xYSvdfX2}nEgoM*Q=ocP$`P6G$B zR@+xcVcvf89Qpx4fuH=3y=zW26<`aQ9*Cz8Q* zm{FhZxZ_S+OwSY;0O7{jO*?*+2^*!L7mzZxbmo0a*RRi;@e8&kKB%|pbD^%X!yfBA z;!y{$zl{bhe1iCU9*DEl->sx!K>92G!dRPvw9AyP=FjtPEv5|aSA!$r;gMDvO`MPQ ztrgkcYCR>X6*@t(kmvZos1l_qB}__|Mj%c4-3S$*Q!=gJdwbegD+I}Ua7Y<6?+@{p z?LwM<@+W^%o_gx3LiPUDuYPs1{;Cc}!v4nJ_#5)6PkpLTIi`W$(vgDg$pgCwddE!X zciC$Nh!gc6dy74#zOXCZ=9a&}Hgs^Y2?(8iaowb@QU3#|yW}Ny)X~G~nQ8mZXdt5| z<#MlWVgeUt`a28eNe6YC(UqgfISq&{1cuiXWX(SD>7Dg@T@(Lhbgv+M-qn>1_vR&YUqDf}I3`RK?+tmyoBB-ax$K`|IcI!|p}5nYF>z{N3GU zBb>;Cp7tW08l3=Bhj%F7Bw^?v&L#dDK35sXSJQ;8uSU9B1cb$Ot#}jy@UmETP~{W2 z+`OGIUk}ovr-+aBmDGVW=s-qi82zUG)`ycTHx^m)YshnQ9)HuOvM1$V6cB}U{!D%v zUZzdvz9=otwaz-CGN1AQQbdt|J9FzrFi}DA5 zptCAV|8BqI-sdmL0&{b7*4vmK3J>WDpS|j2BOsVRFPtr_pjjtFrfW)|YE?3yJX`>< z;IZ*hSu-bb%U;acqjAxAU_jdm>t-U-6GI-!0%2{;`nvC+lm)`0A*Hw3e>)*SG1(O@ zUWg2_V00^wuyq2g>0v_z(oNh&96N}`=-8xOxqRWs$rLd;S5_9r@;mS#L0V$Fd$4TWJHRzW7Ht3%{oAE zVhJNm`-N~pmV$GE95}-F_>%+FpY`GRa1ciP9mK{heCC1P(o&x~1-pJ|`O=Bv4hS2Aum*v(UORfNWak5{9wu!@Ss5DBF$3(kKG&m89lID}qj<%_rAwD` z%dJ(tOoRBtCQYNG{{AWIE-x0&f~B_!cphRKMiW~h=wngL&dtig{DRpKo=fL94l7Nj zKw4~mfBkwoEg=lrm*dRLXbYMrBLI<}04IR{4!#3NSM}QND_HEd{GiXLkf)JF6uiy! z_9Bc-`fGR&hk=em{PgCVOSCfNhs^L_!M4f_qRP5glV8rPqC6ZTZ3&tZtq6JY12>zf zrpZnWbkB9i2SpC_?}X)d=Up#&ev>SEhQg33?0IWv*T@bT-PKIdgMp(;&mcz-Kqz}E zVJoG|0#g;4F37z8`ZTH$=^yB`f@ULtl>NLV(M}zpk%X}LTAci{rk5B(Lk}oUbZ~%i z8X#&o1%PWPUKsf}2~g3A6F@-nra8udAdOE<%eAZi&YGhDxt=<8+6Z?uS%H{5QJ2EB zKh8t6aLSPrzq80MVARZCx_y`ofuramzbeQo|Wia>g@z``*wmtuT6fgaXOI>9LuE(I$w~bZP_& zLUq${pOWDF28=BH!Y}-S{LIh%j5(MP99{*09Ssfz^2#ccU-^|^k$1e~9rm4PAAR&u z`A7fgAIVp~@)bLG6fo+$xJWr9CsLpxrNe+U-Uy)MbV@)-z>ew`)6cPy=wu$SZ=+gI z8IeLZfDzp+o8#^DtmU0L!Z!GM`A@^hmO2*L1g5w)Of5>lG^Ai(t&>929X8Zp@i^=E z$mjX6Xc z<9T^)mOkPuT3!Kha5t*Mpj}7T9Md-1S?US8+>kk-GNsL+>T$5eV{Q z7q?$T^`^!?{jMhxkNRQAE5J_cmJ79(er72y=CR6>EuQDS;UF-fH+O0 z^C(20j`*y6Wf86CJ;|Dxov}~I6ho{uE;V&DQ#x@7v3y$Hn+1B%S-j3x)jp5YK?8;n zU<%Zdt+hxl+@he81jjvg_OrlfAU&d4Q@~>2c(Zt%9p;b);qslhAjK@SCQckrjBc~A zTA@`r5+@BOgpVE;(%uSGLL4kcVU9D(#un6&g;ouA7I~sh1yT#VxB77JAmAQ|VUhJC zMfDN6LfjZ#Y9#~8oAjPP|D2`o{`>D&xpx_<%_Gpknc!KbOEPtfbMz#$u?9pFy`Vm$ zBjKPtIk=bURAI9Vj9TbOVM?^U`$-MHozw3awa|dX1}^>{Mz3s-iyT1_Z>3ksUEheN zr!1QrtFpecaX`o{UmCv_sqF3Ssbe`HLz+h;%=S{?d{UN1rL2xV)Ap1ovG&PEob;3L zj689EVqnbUyoEqnEUYie)z#}#oh-?VzwrsWW`1zP|h2@0Oqc`JcBiDDPkUwO^AT`>`Lh-_4+EVr7JG;-H`lL5CnOya#D@ zHQPbq*)wO(sZRSlr%nRCHLy0=1O2LSdkxb8>0CiRki9Tkn+L*S8Y=e6^k0$XP(x^M zyX`iM6ZxKG=T-ny^L-N_AGJpIECMI5)SW<5jzYB1`|up^HzN+pn>C;@xQ8l2(5*-j zHihYv+uPZlLut@XprdqfwSwEW;il76<2>b|JsG(!6J8PG*b!*@@*G_dJKxx52l%uG_umjeR*=8o|&~Y({ScZbBuw}ICX07 z*pA5T?2J{mR?`K&5zLl7P8K8tM~nK#^h2i9A0<;Dl9ZMq`3N|5>XbR(q@CwDaD?e8VlxYX1A~p>2u?CDt8#JSvi`ni=dP|@m+PwwCTl>s{O|vtpOA-N{;-u>49X2#0hvb6 z*y!OCpZJ80;815i_`wgFt}tTt$xnXL>H_5BM}PE3jhxX?aSq3o7XbJ6sSc=vMLbO9 zpj@r&wTwzCcW?N7i*rUmu?6uoyEM+6IjxQ3HNC%>!<`1%#nw@e=yc#a=SU}dL*BO4 z5v}d=9;HUIN4kUK9&dn=2S%9sw9Z_=<{RzcxKgLcYryH#r&QkF8AI6AC7dLVPJ{Gw zP-g??)TWXirp{7s%s^r4To~ZV>(=4>vOwMDc_Ug)(tWoZyhP>iq*KMp36T4c1^))G51qWnt@Zt$n3XJy`|R_4b`{cR;a(v~M< z2!n0QtPWP#`K}e1!bZ8ZyM81FJ4q2|CwdTZ3WLLru97xJx9E{#8fK7Z9)f&$J}&1h zy{BG~e?}KV&v+3q^2W1amra~Q$PgqeOkau}2%HW^;27EDJkR3zaZrcJ8@m(Ifr6pO znF{x?Mu&KqI)?Fj=}TW~qzO{B!`96-Jla4R(b7DSo^Sw@qsc;@5Pc4Dq^AeA0=f)Y z3Y(VH_SlI@4`q6%$ERg-a7>2FL;5)?LowSBtLfGA#DbppjG)YTO| z^z)j1_jB{>i<(!L)8zP+KC>&UO8lGQo!|K``L6fAPwl{LgVWs%*a~cT&_SCPuFld2 zKJWqgSO4l?neIcfA#vygb&M%vkS0b7Dd)696$AU-rDfL4@q`qlH&8%de0MIsL-^4H zB@Hy*s!zllbhNH_d~$Hz>l5~LVr88%F=AhY&I9V%DyE zq!+~XGX{AI2={`JN9tf6NI!L%G9*tNY{ua`IQCeZ?dzkxydV;-$X1+A>@1_u;L7Fv z&5c61G2(MAPUGMkEk*#oK6d!MkgV^^hO+M#PUnaE#PJmt&`TjX8EDR z56qpq?fFNtATVS!z3z!d7^9Cf7B>WWAPkhZnS-7O$OdsWTTF_Rz`8AZ?wCHfy}hAn z#9$w-XSCA@`45Pl?>sO)0!3*x!okArMRoj9bjSfcsMoJMAw6^EtTj%=4a;n^S`>_R%u zxE?`DAvQ-Ln2bRAl$|SDrnN&v3!4#e>hxK4Vy~-F-nIIQK4MGwPQLh#@n$5F?X*b` z=V)MXsu}5E%?$PIUGI9Am9dd3{kOfzu4w-9Ug~LZ207A34y1qQfzhRd>3TK_H7Tb@ z=Zrv24Nb_@;JEZxeD3$Hgw37JI&n%yoSZmab7G~((nhhx{sCj7zJ6tCxp^JA{JrxAKM0f6mHwWqDO~XWMj)k@04uQfWZ4Kuk!yP2+uU zj6RjriJsJWs_rzX4TnGjUIq(siOCc5L-+MN&Nw6weLF7bT@9TR`5WTYFJGFlpsN-;A7@`#P`*#?a^omm|C z0qATq#QDnt*D#}yEQjZd=a5zUGk^}^8|&yQLC|(2cF>V4gxg+`!?{#iC_fzDaDN&+ zk6eRe4|#$l5FR8d54MvWZ9tCh-m#cp%v_rIFeQAa1=`+tSlEvl{7`K&VDn$SdBfXj!T7P#$w~VM! z1L0oM%6od&R+lZap{%*Ll~B{5_iS4$VmcjZkcNsnjL3=#KpJT5Fu46gqw>@fH)dPa zEX2AV9Pw7pKE3udY*%td@B-+)q%mJunwOF~EVti&yRF$Fe25h?K&f#GFlbcvJfsOh z7Q#JQwk41((p{7^;gpd6<4m!j=NLIOBUz-o**Z9oRW=M+)7cQf6d-zov)23Yd|JOT z4p}ngUV5A%Nk?6e2GPRd;5}OCJfslf#`oYfQtp%o1d+6G;J9E*AXwp`;p9Fx5g`wu zC!B4)SuYbDMs}OSc)DJxF)P<(YjsNvtWTNZKAtxNYI;(aQ*cOZ2R=rdl66p!s9F6U zrtk4tDV@L$e8yQ6$ zKQ1qM=}WCKFVZ*#U>_LS;swU6#|kQ>vW)4)TX*wPuj;x{AP!C^M2ZZKj2 zd16;RMnj3uiIcDGyO9X?r|4)Fddv=XyoW9jEjoL!^_MSSY1ui}NrZX@7`29MU%dF7 z%F<3909(OYU#2@!KUoKgtpI^+#BxV<0s*1y0s53TV<3<#o(bRMwl9zs>{Q-#Lx5jl z9Y6hpMz|N|uOG?_pwovJnG+6V91t&45wBfi`}$@bik_A@%~=R?aU9l3!iivHH=2$)uI2yAAHy{hhSdwhPTScKl&jvenB2tPG}mCr9mY{mo7YOLS~)J?k*cC zw01<9)<^?ML%x=5@^J2syJcc>TE6u8&sZZAh4+ln(^{(r9o#pW#zr$BEf-`Shs$?Bnki$Z;)T(_ zqX43XBg=Fzj22E7>)aU8V$|XE=`-f=L4K%D7W3%y$7HrM{%}YH|Lzg=B%70 zghWPQk^zx(veKhm zojWJpuv)%Xnmy;9GiRo!ySlpGuIg$>uw38aJ*4u$fyd;|JMR?sF!KZpTyxDevTes! z3twXpW!Lehrs-TQ!i_q*Sf z-~ayiDzD2sII=+>_uFx|zXfCLG=ND((&j(**kd*}pj*SBJs9J&RW>lr zhIOn&2Q4KOi(pa}?-S_nA6yuZd?=kqyx;RVdeEN>IVD@LCwPevC5J&Ecco<|X5jH0 z=(vi?iiGR2oK5A*$}31jvVi-3X<4{VxO{vD$`P41YnsVuXW+iqgs;LI6*9UWcZgKKOXg`_eBqo zE!%mWsGrp94_RQ{QLv8qj(Vbsmee1QWRL0UH&@Te*834{0`Ul5V+lB!`UxOY$5Yln z)7}e2>$KM&?dJ5TU%nHM=u>~_r7%%NpDxs&{v9QM{1MM1_y<9tP=poCTa_JRU8*9Duw%@6cOiDx3M%l1wi`;wXtyVd)Ixqy~BkJ?o zx&|Xg58QjZ{T5=xS7b5-{=DFCf&bhYk6HS6hBbK8#?4YyU8~o-(>e?WM^SO1U3+Yl zkOJJqWQ}pZUoZIb1`X~+VD=-wphyP#d$oM=2WgCK>U?1OBhdj!d3F-m%l-Zvht-Qw^+(@ zN~9vcRI2jJ^_2t_`k=^$9Wfz^kd@N~lx*MVfFn+xJVB%^sf7EiQ(vc*G$w>LKuWuUv}B$^1>Iq(6Vp3MZ}AHFChbrD}Z)p4c1LJ-DK^KH!EyB zdf?GO=Fy+UrKLus7*wZCk7C?8Bf$eBtfr=R88Hh2QWIX|^mxFp&7w~*!IcU)kMa{f z%Va~m%%jv7V+}`e(jryRb|BerYinz4^5*E#qxLuL3`ycO48Oh!V+gtM2_=GLiT90- z%~tN9=gT8VLV~}G2iEhkk6kL85QBJf7zA>RcZ@oko0~DCHr@vJ-qs72b19Kpj>YF% zEQ{|uQRVBji8B{^hEBD8@lF>}P#|j)c=oi6W)IQ!vGM}eAMq!X&B*$KcUb|!N<_56 z6N}E%;5;%jYS^xpC}5ED9r6ZFfBkunp!4iU)gRw8!AMkBWc>xm6nfX!--znf@N;`` zPLOC384~Z@c(x3m`}%wQOg}rxKf7ATX#rf^G12_{=1Yd=gP&GU#(=I&o-6qY;QHs7T%RZ z2lko5-?8Uhy9O0?{K!EwhRez;WS5=?K|m0X9zLKAk)_w(Zdsv-8{d1+?NXzpYtxqP zb{-9K__0T<;&z>VzMeB+i(L7ATk9z~dF+rjR*vl0d6xZdbaX_<)Bw)YK?af)1muvi zBLK`o5|W6azYPv@vH;@6LUEmS)>&F^ zmn9Kd;9x+sfdl#1-*6tY-y}<*9QWRPuiSaZ9Wu_WQ3TH4d5%2rU$=?O=mWAq9bL#P z^-lfqs{gx#e`4>~39NJK8yk@Jflg`b@3f42J(H@7`knWQ&({i%8_CKmFk;`^->Zg5 z>IOQxx>gC!#TQ>9+qP|!E3bT_5h`X2(V6H1(z)Mv-+h+YB$)vZVPdZ+46or>VjJ|U zH;AIZ*i}_smC&<5KY$2CC5kwmU~u*m3-pPgVGs}WA2N4hR6!(DB~aWe6)zWUNWb>F zISS|?zNi0^l@_#|I%%&STqhpcqW(E(tG>P-H4-7w&M0G@C$fUKE^5*Rc#;B=kt~v4 zaJ^JEAx?#01p#R!>VW!)B3Ygoh2cq5Jd(KRv0E%p)Of-5DKnFCE)2>(bh;PU#X1_g zuY29=<@K+7UF@&K817e_LUm(F*s0DxQq_=$++rdbPN;YjoZ(QyhiJR`=zdFQ|uFW|B=Cdso!=^?2|=I4t7a@ zPnV334oN{#sS=4iC6Oc2wDZE)87srdefLOR<3?*E5AKs&SSqy}wo0SgPXF{z|77jS zn&dBjv~{v`lt-gY?4Z;m5lscx&Hd=ZBN9He)OZ# ztj`Tv5AKz+%G!lz=ura0-^JDL`$;WlfBSK%sIHX}wKHgwYUL*j(6OrJ_+j%pL7JMk z?uvckKwX21$|{*w8)|mi*+JN}o$W1BS=+coCP6OS8&S9UWzv20A+z_VmGH9f;RpZr z3uh+Dq5=tXKtbWzVOBm0C~(*{tQdp#k|$g(_ug@<)HXCqaZ!m3jr2=tafKW{xKA#* z;%XyJ-Abk|y!1*L8tBuGc*s0TTet6$M;^FWb}GSYYdI<1UG2(18_e?s5n`6hd$OqP z^DesFuGOav0=X+LEtew)AC(I(eu7@}p!BI>4H?<3r0b4bZ-ug$7& z*u33l>cS)|2>KXt|1z>F*o+V~iSfhnu6^}%vO0qaFYL*J(067GyZW2u!S5{V-ft9L(5(ag^Y%!hX1S0Z4*rg+2V@Pe0OSo)v=*Qq zkQt0Uyb=6;&N=5;{W4=sG8M=auN;I0av*D@>i*J~z9c{Y`Ogy)anN_8D%QgC7|9A8 zRNoqrEW$6kM|!1uw9mXv-6QU`Z)!k##`~pL%Q!~gRP%*NhTh(8+j|Srmukpsx_G=u zR$fk?t;^~iP?EL6K7y=a)HgOa+eXjKBK8jqNJo2O?-<0&Cp;hxIs+O6gz(fuT07X)=Z zIWf85$qEA-g8B|$mv%-+pbH#PP(xYE#+ZTkDw$3I@kmM#!K4?-kg9wIVd?ADP8k}a z9m8I?#jqJd#QqGTqOa}!}&pR4P{V@pYa0wPh}yu*kg6ANdZeYV_w`|a{4 zfAVJe=}&&H_GyI-Yrh4QWy_95!ZinBnX6MBpnCQAu#$*Q^UzU$v$G_f%8|>Teo^e~ zrFiI$$0SSjJaXa*XS{RQ3iI;~^7gmCUB3L~FUxCR`&wIz`@Z+R&;EuukN0<2IUrF0 z@;9h@0dhPwmm#BlohBdr9sDkcmZJxZT(zAzEH#artlg)zU-CP^c#^N<3v~k~#)kEN zbtZerCj@KbS(n;(L7n8Aq~=Tour$EF5Kzoy6-6VDsD`gPu%9mtFZJ zIr!Mas?;`EI2uIi@~fUEcieKLt;f1k2@e(VgsYz}k3ITe>?$HV_M9V+K6JkkCOk(S z?WeTjoskK_^WlQ`-+h~ruWdW`D5=_S-@{tB?rt;vG4rRt%I@8w!qsK-jcV25Gk5-r4UZ;W+9HA$dg;ei`A*?>rj zTZ~Pox#br5zz05H9*tBCo1cH~d-9yipShG^r9rYL zrY59wutz$Wp`09)?lC1$Lp^e$y+w| z{W7O8M~df*tbYtojHI0!K3zPX;ezF*1r@DHD_`|m`fDNLYB?2DlCwx z8B+D*anaY9(aIXCN&PHYifj+=)u-emUf45)0$rDA^Vrh~zc!3e=6L;wF}X~2PLYnG z=vyXAcuw>yL@gEg`vgHU@-N1Jg{FQ<4>~nSgGitFeob%Vi3ga>|7iFf}yJ(oELR{K2Jmx^=rE(uYgPV z_rU)hBw2h&-(ixP$Wdu(nfJBktWP&X~v`z73L8b~ys?0+=t+qm5PL|yx*yg@n0G=^W zzwNw*bMV4K?f@Rg%7%^ho{57$|Hwa`86*n=LAVEoAX(DlW%vmKqmY?=3_}cgLeDO` z^a)00Sn~t9!Fas*@+T^Rx=+qN?_#;{E*4ME$rGP)jjbyRs+GeB_L=glRzg)<*Ju^k z-gZh(9Y4B+WYsrrlpVVWd0KQALUvA_I3nj>cv+0d%`G8WXP@mN1ErA#1yK* zU1~-?v#uCA@z!Pe@z!$*+BAqD5sCNQEV`y{NZ1jCzZTaB{uV_GG2r}_hRxA7JexOm z!k`U3xp9BTCsko^F5aG0J#!vzX1G-AKi^~&V}=<`!uJ@&tCWnAvY4!YVNWH$^WLT) zfeI2TjMCMD(86zh>s#{q&wt($aHNWlyy^Y&w{QJOB*|i}rg@%*@j{VtPY=9MV{Y%K z)}9U-8XHz+IV2-7BfYdz<+E>b4Jdl9rdi3lwwotcb5pan-&SsMj{OaC@^hppt4Jos z#@z%n;U@`y*aXfsGAAcQcHod7XG#Cd*?cw~hX^GqBj zBV;QfS&VbY7|*Vf;Z&_uv*$YfKg_C)34(Ib)P*fl@Q`2(E z6;F}_`yQ|~`Tmh9F1mix)}3KJ)w>fB23C&yDwd1r2C`?uiN&^w&RK zh$4EF1J4a1i;&3+&pO9K={z8m<8w2rr@9x6eY`w)ae`hyexErrCiR;hi~Ul6J9n;r z7k`~+S9`0p93EI8eW!zD5i&T`y;a_;r0RLkd!D@fAs z_MSRq#Ebh^*EiYs-pj>z@BHB3KE5OiC2DEGRcMCP$WDb^#RJM>S_3++%Y>ide zll8#8cbEYVBhFWaKz*Ed@#T>uYx~Z#?EVKId&HFHhRs_oO!2^@588P&qGc&rA#9e{ z3=zCs*;alOD2o->(#u7A;gP^FPvq&Kj$PsiXC$+Jh<&rj?xHQF*U7eQb38YL0PsmN zf<(j1q8~IsKt59jgEO9`L<`-6*X4fA%^PgsO=Jut1A9Ep%}z(w757MoSOvkPLZuGE ztI2*yefZEHk^tbj!;8hNE5;gLCr_$4kgwB$0|ySszyJHcTShr=1gA0!|Ag}|lTUx} z?<}E22ZqZ~x2=b2@9U6ZRe~&R%~WzUIWj5P1=>e+aDtyKC_HTs2H)|qQR!`4K7jz~ zDy=D5AXlC|k*XH&)!fXS-6Jb6Q%bVk%$Wx)dSZHPS~9XS zbim1$xj0^&RMCLmpP5cM!(bFLw9aQIS&+Wy@tD^>!+^$nOI{JvXNf@DrQ>-*#?h0B zycdu@CP`MwM09Ow7@SiNfi^V+ZF3zst&UlVN^BFdouvD-U!hlU7U? zTzchGq~+u>t!wAWVkRB(84l-UA2%jpiG1NXkrhB0yxh1+Rx%SaUotC~JmE=l@X-gf zFSbOT)(oe+0I{YL=imz34k0zTEiZ@!SNwmk41R_o)cSU6C)5>q0+Ci6^}%eR_>SL z0#_eFFP8`2u16j`vJjegx;$ABt=#M<+M28%{pd$>4R+Bb(XZ8I&Q)RP-xqCpDm|Uxsk>e6b910$b7AcjY^zEz(1{hS(`$OOhI_r!>I8-MZ}HExx-1%+=d!Tnjo z5pO2fk)5UYE>y)mIl%_fE0;?QVA>W#AY9LsiD$P8N)s7c*ZDdCB{EO|eT*z_J9q9h z<$TpuS6RCAYp=c5W{8NJf76@Ztn%{5^2}#mW8YtV@x``gYWw!>ws;<6G)h^m7w9*; zckh;0z3Nr6Yu7F{oXafU9)k>H5Mv;!pLFzgN={>;JbY@u9P2$Ptpgp>r+spAddm7U zYgA^Z@jR&<+GftN9|pi$1!R&9QaMyoWoF9VtGudQ<|b$Lw@baA7&GS4((C6I za@Gt>$}=nkk!!9PdZYfn0l_BBQ6nu1NKmSv4dPLT$;%% z3}fV<^~z3vsBSARk>c27U6hWa{OBM;r>Pr4#Q@h~oouka(BJ50Ug`I%eRfCGTvpvM zxCb@^_m660K4+YAb_Ll+-QIf02IPyiws^;)+Z!_C>zDV`m8-8@GfM&n}n!4XLD(*eeE z)^%klp=57WePf>w_4i0s(^ius+BE3JZa>v(&+W_D*KtC}sm1kX=ue1zS?va^gD@t? zYjjL)uw!PrK*z@4N?vY&jAhL#xoDUCSh(G^lDe9v3aziTop61ur&GSBHv3OI4@+}q zu2kk$%J;wj8~N3*ekC9Lzz5{r?|!$*=QE%6Y-u@uRC4l*WT3M}nm2DZ`JfMs6PmAe zv_T0zA$#%KZCY*kNpy#u!L1dYXBUoXi?a5*d3LXp;W7NzQ56&J_C<|r~AuX`G zwE_aSZO0xXHa=Mfp@?MmJ9KcLRRW;gwe`(PipUf)F753t=H{bpwGGWuUQs0@Lj!gl z6eKzaZxkK$;6AsOtGcey2o_<7-5qTG(V^`%%b}WoF*t!o1NforidFP!c&wu{&<>^m<`jVNyWO(qw2jr(e{h94S zGpgm{T7+Ht@Js&+5D*4Jh#UgLAb{XIVRlNH^L_eC!t>g+UNUrWD=8^96738czb30v z2Y6=?)GImd)s7Z#eZ*=(`!S=~-QAb`YuCCVP*q3o7z$m~=6wmWVxmE&X zYvaaE_Ub)NK4dR=FwmTdU_c=hlW;)v#K`!F8XoPIhyc$4h97-_=c9j^0iczYTbL^c z4;(ZjoOH|GUETIMa*2HY@wI;}Wff&+(2)>_Sz0_Tc$gqs{ycvG|GEs?HodJ({!E#S zSJg%j_CZhY=!d#kfleFGM~a;LQT7^5v|jP_@to8hVQr-BPBwWE59lW9CzX&vZ>%H=d&V<{UR+DP#B&DWL)hZV zVF=#q#JG#DCwhVPnS@df_LD#;{&k3WV635z>33YmgL*?>a9!Z`%q!M&W^HZiO2KvL zkIB|Mp?@KX@dz~&FN}@8ZLuME7A7%hLyVoUM=M#K(iRXH`nVhC-TD4H>9$Q6cn@hW zu8(Zu$$IGiyR}cd2n5>-M#rNq+jp5Q!C1?BaP$^S5^lcnmo`z$T5*;eu%7PY$2bxM zUa)w{3p|j+u&(vML($jOX5O%p%9>?szW5%mS-kMXc;K0bl{}6QcAJ+5!<~tI9}<}; zxd&dmo|Z#P>Wk;Z&}ZFRDrjuFU+e0M*k2B}|N5?lYvNVgboO?suTPwOCl&mp^$|HR zqGRlg>(3R%?m03irsdsR3*~n=j+lPC@=8~B5`Bhy+Zt4XkYb$F?1#YnWMfSP#lCaVxaNL-$O&g zMmQgQ@IjM5*PpWO6`g^?^oN%re{de3SJt|0QW#t80d#QxK@01IRPEZcOPV*i{c|An z9PAaeK(e$wkumeM>OFm#qd?eS+o5)o3ql16366Je82N;;5U)MdYhi>d#k7`npg%gT zw42;;#xeC$TzdKmHJ<*m;xZ%2Y;Tb^^cm)r9AH;N7LfCP)e}DAAYFLFJ6U!@z`4jl zqC^=O$x~Ak3C~FvLA@J=WHN?4`Yf$ZCy*Xote?drC_x}Ec&7=E^AmcID~xMUQo^<= zZY-f4So6y|=aps~C2GQLV-fox3DCLW5N*abbRLs3!4O6W9J9*&2IA<67UU?^`!zK+ z+Qd%y`GW0~YQGPHI>rO%2`VIuBwSq`ZE8Qb>k$=m;^@JJwOachdBECtv+oyO z_C)#qx4&+7Sa{&!eq?^+)lWdCL$o}>0)&&*Z#i2H%;laA)(eqjAV+m$D&YLymcwSq z`Vc-JHpqG-$Uh-)7|lEOEUw3DW4n;iVKclaU5ECW$0EDe&$;@J?pn~zw zujfhy$w(GNrlGc68g?xuzEhKdE?|Mj6qZ(kr*LG+LG%Ez~#uX3{8!Ug~9zy8bm5RLs;y2(sc~ToaK(0u|&LZhEfn0@w-f^9x#|t{5?HxtwwcqtzDCHU>r6yG@6m@jRHatsrQx6 zV5b5GX=3;h_AsJI7Q~OwQ{7uD9!Q|UOfKZZuXBP#VSM=aLGKfT^O*>UOOQMTUxT0f zHVwnenJ2HvCbHlO76%?rWCy*0{BU@3<@+V)msOW(U(7ZSNKXd~;K$7HtZt~5y!<>> zzLVzd8o>)O&MftW4LHt_aQ%d97FX*1qQdZEU84P%Tc=UKu}(kATnHUZ2G*FC7M2;g zo;1TRfl^6;@zJq`L>z#+2+Qw6z1A%su+|%-ac8{^daScM^&}-8wjN8{u)en5+LdR) z;7w%>UOdpR=xfMpA|PjEvQ6Zv#*>BItfo9+@Qm@~A<=h1;9;kqF@b~e4q=Md?}Fet z=mYF=<<_tDnM?soGNJ^=wtSeKU&t6qhQ8v`ins|!62W`fR^kcc86kn_g{Gzk)h)gD zHxn*Avj+sRRLK_JAKH?w6H+Bv)HmMKR7iQcV141>z`z1|^Pa5ZN~(A^gBG$=N!PBk z&Qn`0OZGp!*V=#6rcFk&?!Nsdx#H@lORtht-@dHXA}jH^7hW#+-*dYWVIoK_f8tZ+ z(Ef*HJ|j!EY};vT(h(*0{)A-x;Je?jJ{ASq)h42}?mSW z0w$s3CHDYP`)fgp2%+|SNF_rQ$+Eq&_`P12XT?t4d0~K{9B7rpM~*LebAqYRsb?m# zCPxS4tPA!m$zu4a1CPjD{!$MkfMmUA%lXov^7N};JtXIzbI!tgyO+N7rIu|lD$5{x zy7SagY2J0=l0DF9FVenG={V!pP9X=RWT)LIuP-+fHh7>QreH}M#z3Y=2y~Fl8nlhu zP92lRP1|E1IIKCz0=0Fu);XEAwtcSD075y2jo32@0l75A6c5az(ScL-Hbo(LuP_L; zW57s6w5azNzxc)Smbbjc3|2th{`R-OmFup%&PY~N{CgNAo_OB#o^R`On6c%$;HN+R zseI)t{}wm1Te&K26mWQx-5_bqqr>H}Pga-A>SuK|^_B^ba`IbiK6v>Wj3|Q5EGg~X*_nJ%KZXD#E=ZO| zwjBO%zT@B?C^!EKc;#A!rG>(#=b71V&?iG+Pn(jrp)vcuPDvJ?t*H@`G9*wCF=Xas zNL6W_8bE_G&bq8bN+k)pJC-IlDK06}fnw604L}wNkxXK*tP5B_P^W_xZPVI*D)wm# z(83}Q^!fapLaiqZl^GpJ>TNP+rCt+2I1MtAj2IC*$J&fUo@!(ek`d*}ax%Hpn>;_S zgLZ<@LP$qqu(p;&1gz;@elD_${-W*}TNy9tuKh7TLUIhunGF@04Uc zO&*JRxI9LN2Q8^qIwb3d-~HyYP{APRGaU9K1+qd}9P!RVf!pUEfBZ+!kUx3T8&zIL zELjDHE7{$mye54eBnOyM-KA}&*Kb#2c=NeWh<%$F!PM^b4GiF+zAo8$>C>d8wRIt! z%mYsr^**6C(3UM*%-#TG-{X0Zoz(!cL#8=zfD=MT<~j`fzRngMBL87A35k^to_9Y&t`)_^Jh#Go>XCV|WxFK;k z17Q>(d(GR+rM6+QeC=bS^7&&AMtZaMWKYYpY6~sP;ElK3V(Z6)@L`h32ILO;gMhQP z4r0fCWz!SmGT7aswzSg)<<<3;9Ah?P%K9c=ui~l(3(X9M9RkQ?)c#svVYA{00_OMM z@}RzwCB9e@$-=lqDG#VoMhHpZdh3oXHS7Viqv-%c3L`tpqY(s~7UG%1Be$ANi2i<2 zKmf|h%d8xuBcn@<1?oIVuxKni0{`(J{~@>Cc3awm8RgivZL7Tb&2KSfg;4<@T5JO6 zvjYY}0tq}%%s6{P51{Bh>B2xjXuT(ktaTVqe2+H=;{avl!646|xstt^QUyeZ8N@_& z6qHqrHP#oRunFCxzL_QWfH-|!|-Pfz% zqA#d7Kf8?#lG%Zz43rm{rVQR7qz&>3?aIor&*?W~V{Xz0W2UgYNR?^7^+%MNQ)Z5I z@yfxu9A3e3a9_R)qO!hH2b4S+?jM$sfe}|$(zYwoB3arOD%f{J>vMQwcvTq&r<71J z`9N7Jsu$z&?YY#T&Q+rFc!CKOjCInuqZbna&y_8%pL1(-Aq6}e!~~sDSydrjDtif3 za8menN68Fl&Li9|QN~ep5}wZRwdm_FeEI9O-z-g2O;}x9%L(b}>JZk#1dZ$~u6mlB zbNwjwc!<%VDU$c*?fAe2`<^%`57e8aDPWC9MvEI?AOt=o6Y?YG>hL zMv<)l`PP4EJLkqFS}cEXNnRE^A9c&p2>ND_XubB;uaT!+e$Emj6ywvUwGIPgxVB+~ z-aFHIe$?2kYF;|yABA8_au0O8&+HZsp^E~Eo__u7Uz_)1+fGLgAYveBzoZJ%a^s!x zo(#W-6~LzHKKhWY+3`lZ)r-nyUP)zKFBZBSuMF$QAd!TuQ9nKnepKrA;CifOp>rU` z@VOug@|18?PfA|S*4QU*Lt;|FTN|iA4eGrvea51 zK}lABcgI4i_ch-qi_Xpfm#qqUV4%+opVNh`{7lIy%$9jovYFW#GTy6-Jw@9lraKIQ z=U;}Bl2UtKVx4_F`%00BXcZNeScoNydf9ZCEJWa`Pkrh_h#(Dh*IjqXKmOxC%BfRs z7MV7|FnHnO~XP<51YLuCQmmT--x#u3a@x~kNH^IS=420KS`w}Wab8k)_4; zPM$dVa6Tps&O~55K~#dIDNLe*##S;wU}|dXwFAtWfe>C7u~xt^@44F{Dw359=_bBd zYgAQKn-Rw3#n9N0K5Kxu#@ZCaPARgsAt9jIGBc-Rht??@w0c72CpB7I=0G{={Yh|+Hoed&^1r%`bjnlZIr{<$i#Sw?F&ge_P#gy&(SRk6&gR>Hq$w zU#mg?p!U)DMk?)GR8qDo0>g809in1b8_c@iFgVA1E6@++Do2c)=!az9Ok|EaN+nTB zs-Q10K88J{sgSJ49(75z$U;csi<`84NeV?~;{LuKlRL2gkq7lTD=o}$pufj{TcoxL zi7&9dHmac)&yz*n#3NcvLi*%j2HIaQf*g2{)@z<~wp_3^S2kSy7AYxPG>{=1bzAmG zUcpk5MW|rMu}2pSVb*O`Z`iTyyeKS7Nv`lAa6U!%#!bt;4IU&*V8Zy5pZuit6ZT)+ zym_-NmuPQmv+z6CHnnSeqynN!Vs^Ol`wnT-AEpZ=+BBY=19wQu;-wIf*>8M78*#el>N z7Di523ZYhOg`7e)^3^8F;vY4RW+wHaCK8OY0Z`9TA!A{bmX+C_Ta%Mx+G$r$AQKTS z6eom<@F@lv>h{y0{0{+fo(?P}(mT{CbJGcw z$|~`AlPofu733B;ec#<>(FERG#Y9WC?VgW8Leh~*dkq@@9`t{V<&~`834`!4_S#Ao zeheAILqhvS0XnsxFhu`^XtgmDli9mmycgYi_v1y|`wD)fFc2I9&lI17B zC@aPouXqlNikFhC%*-6AuBo;A0St)Sltf|hbG;yfB$_oi{xe&y&XW*{3QISxgtK#TwS}I*Q;S8?1W@ zlYc%Z%ER{y*H&u7#LCV06_su+>)=4&!g<&Pq&z1?k~(th(Xh){yU8RKy4Bed-As6f z+S3DBLHE)2@%jSS{lJ@_EAM&D)8){8M1 zf%QH=RXgaluYIj_c64aJt5?!Wrpjn}AS|!tz+S1WskcNL{YuXK9#$YKb6qOlB*-M* zE^m(ny<@(uw`^VXE_srHobdgreRoTP5(3}83rQHZox5axQYr|p%YtAH=$Lu<;g%)d zBC_U1$r9h;W!!#lvmDs#)@fBV=9*1WURkbxFNPt4TlXK8pU-tH^JYP^o|{=OS8VT+ zw$_UhlFTR>2_qcFw*YM)Y#dLb@I=uEC^zjbcner7!HkOc8k`Q~sUls_bkdS(~( z3of|822?y!pZLTlY~2iC0rX$})nCcWUiLB@gn@gWZ=#_eqt{$h~m56^)B0IiT$6R@r-B46Q1w{^Xjpn7|$MqFwb$i z!7L93(7}TTW$)g-mO3A0|HB{t(6Z=2dcXL^FPaDO$3OnD+;GDU3rS2EELh+UxuT7! z&tUzp1)R)j|1B)AHfBGGc!Tar;JsK;?H`7}FZ~0g#ior>+?!_Y2+4}@coW!6&4rR=Xu4fU&+*K&Pi@jt|?*_ zYun?A8Fr9Ul8N+xZjdddA<`>%F8m~;Cx~kIpToA~@`4?ZV&z@~WYGiov zd*p|t5qr+P&^%9MfJ0UZvxEE*PS_C({Q{5yS`i{jT_U@MI-c%6>pV4bCgsS%eOfne zOMp2&kCgr-LLlvTzJ7M>&_4Z5wp?(@6HHd|a$sQNfk#&|esFF24w?7p*Jo+jnCU)x zXupMF%TugBxTF*%(%o{@k`h4v2*v7YIV@!r)ppLr_^3@- z6_(YQhpV^s2|U85y*E!-M@&*|13f=Rg?0hO~~cQ#_Cp zqI9P8H>@8-PX6hi{z?ArU;kC^zWZ+Z_}~AXo%g!ez0Stjn{K+v`ZZpMY#l592jLXO={gA!F0MAvou|e{47T0L9M1+0u0P--eV@!W+GalR#7gqs-K63Go-$vN-|V-0CjOn?I`ry%*2?MO@{#}V zZ=}DU?P%ifla&_9!jt6(NWx^8b%VKumD_qDuRK?UZBfyvnv(fh=N?Z5{+ak?JRsQ2 z+#$%x$Uv%~;$0_QeqY*zIREo6yhvX8ikF)gfb;+5U;f1mZ|W9;g~tgZk*;S3qxXs{ zu8$I>YW-?5h# z9S`Gy^BIINnEfnk5X^exVM1WZfOzSpms$oz6dy6UZ+OESj6D6xpZtma&Bn@vPja3o zP`dE^lb<0KIY-opR+pZ}4seeG-Vg)e-;*3OX~=NG^Dh1`7e z%{D0P+qX~a_k{gE)euw$9t^qQz?urEmZ$^y^YZVA%dFm`*fz)vy8l-T3^Zf{;uCD6 z;QOE=;(hR-oX8Icbwyb@kTpnAUa@|zgp0%p3{Kp~eye)O@6*FI_$&ynSF@o;ib~n6 zQjG)setL3R73GK;i#h;k_netjGNsi$JEe6~p~m@y+aoSp2^HahcFovjwI=m9I=E)# z*@ndb&yg&BZU`n>6pIRqEzyymRi*9+brACz9r-L3unraD#f-gG0&+miK1OMxKEmdn z?B|#090-J81L#4!$0J#Q^=1$m8t0J*?lt)gk}OD4zSbRvAd^lI0T*HoQJ<4y)nPl& zI^T@DgZm%S`(;@+O&0G{4*JlR9lMQq?SJ?IyQe2v`ySpaY+?(vm&dFBYpp!3r`#l-hbLJljvkVBC5p5a`T9)ZkgZ^w(ElLxj6Ku??`>08g$p%yW0vpxv@7FTvXH`b;5={f_xf5(@Qm(o z?*aB$7<|U}Hnzmh3&a0@Hcmf)V7>1R&yM}&0MW{v9hArJQ=+x`D!q@}ii6NoNE1Y- z>%;-e#D>h_>FV$8l4>O-S!&o0_jRgmFz-SRwH+nt|=N+;z2tHjb}(iwi?)Z z#pRNhohg-7)v<3xy1Tk0FDJ_g9&L{oVgLU9@`@K-E0+a}8y`6V(vuXc|fQ#x+sWMKo%$>03V z-s0U2XUomK|1fe^KDZ}VxsXd;aYv=D_?2u{I|dT9s2%Nme_-RzplFKD!K5&3$1Kqqhr=F zT@tw#z*vWP;(ekcQUAaB&2P;6#NJ}w`-CToPRMt}f(N}%)ZJRjOQ{+l7za^u8itoE zrgpa3PD#ely_E$JfH}WxXb{L+C5;1A8L)=L^32gT6p{@j9WvC^O-Fm35UW zp9PYYqr_1aCw+kNmoC8UNWbt`E;eu6V=8f-Z?~$S2VcT>!Ggpm) z0_UajgDaCbnR?&gWDeJ*-t)2xWlsCROw8N09!Oq`YXg#2V2rcrX@wdfMfrtVZyDwZ zqRwbeNG%3;tXk6o;erf`Tw^Uj!U`sM63H%lin61p;(=L92t?3urfq{Hi;y@blFX~8 zGP85fIkv85-^2GyN9zeYZ}Zlj3nYs$BWDn^{t%BeGRo_e-G@*lJWG(QhxXp%dZx;_ z*SC0$&b{DLn}9p;=tK4lQ6wu$I-*Dx`kv3w+4tY`dnqj|m*!1d>^cxaM2Dc_niw=kdxI&->im zY$F~J^0fdHh{)4wh-JFaM~eh)<@+G79P!3#589YaY+fgNL=h|xh*oaqr0l!r`%TUmwzcYTz`X2GC%atLvr=gpCm)wJ=*_NPISDaydM5N zUbo4yA*rfY(xrW!z6HVOJ^hXI@R$?^*06aD_I66Pe(ya+L;XEcrS;^m&Dd5`yO>m> zy|Y6y=4N7R-S+ADGARR{r)033wX7LZq9g|bmrw@Jt4PUDN%bNriiff3K{+%uV8rU>N`PLmd5=7?y4Jj0U~aBO z_1s45SHXIKz_RWU{Y)r-ZehtX5``x(tDsEVA9-*jiq4QRKWk*Uti0IPze2u|2M&lk zWRA)3zxa#4SnweJ(H~tS?|kRGY+|3!2}Py9Gl7nCfVF<-oO6!palfA*ec`dk4kaX6 zak_AFzytx_iPHfDaz3yS&m$u*b0OnkGEfQ5=yd%$(6^WGZJrfC zNZzJRo9%NBgeWruZk8azcvrt z-~R32nvk-IG9-`d6aM#j5Gd9LL7vFW2x+2@KK}8KYiD|=?aAPkRUAd7RXr(GkkIV~QbL>L(vRYPqt z3?~Y#HABAQflO{^pLdaxqVr_WIp@o>U+@yyq=bru7!Tg-)?NksYJKAd*|2%L)HQCB z+JoJlXOpH;j1f;+HxJ-__2#ke^DlIGT0SO@ZZhC6m2uip-WP@WYVjT>Vu0ik_ zWas!px67eN?y~o_N|ey+Wd)`3yyrd7*34iGA3EeRV-i{NqUS%?CP!bYWBj|{{ca;* zpZUyZm1o#T^IXwIsDou z4-56xF`2fQ%Po@0xlC)PeytxLeM6fysBKARM%Hw3knDsklO)ops&0roWePz^-8L{%SwveH(e)X&N-UB6r7Y4ll{qL9O zKmYmWy+c^=bivp;+}=-BHruln2KZV7MeUPE_=8zrJW{7lwJc=i^K}sqHMQF5wGBKW zJre*KB`h*g>ggc7Jf(^lA6WqHTdc}zc&J|um*j>|6fjZ=&qVmLnVp-Lfg!T!rKp6`L`JI0EgAIr@yO+6=SjAb6O0_Zb5m0jGC4VI zNn?DHoiOl1p^q?JmKaU{@VtYHc!$w1Eh1R?s4 z46k@UnJ7T-W5mAfb$@C^#}jxDeoMj%vj082_b!`Q;InI<`(jIc0m8olEhmr3FMoWU zeMbgCvi%VjcxeA4S|?8SAdSr6*VZ*!y7${}zQMdtcv0T)w)fh7zW?7}wTy=G`U-tH znD_!i-DfkT+&{kV8%9t_Ie*?om)Qoncij40BXd6S1z8PAj1dK7fqP?Y1OXvJK3FF; zIX-HHGO8Zvmlc&2(%;v=&^MzHBz=s>QR|?vO_+oMT$fFlZ9La{EGa6~F`|3P#Lr5B zp~n7}>#?bEGT;%7sz(s~cwoR~J48RShH^=UK|K*8)^um=N2w63AO`wc<)-WZRW?84 zOERSQYKlmI?UQ2^RX57?aK8}*3m=~tm7Iby8SHkc)dBh^m@$vB+NW+u&j&Z=?d@sT zF}+VF``fMGLJl~GRNJ<`sj5LXY~3pJI^45yG0`VPrLY`cV1mAWlt z1Wc5=J(%Ut1A^!G>`H|2eF21!EP3Z#q69gX_=1gw+q)9)ffWVX3Icc0)r)PE2qf5; z&+L>;S>y^M<4XFb=2RwfWO%H~^zN>mTddwadB$Ht0Gy%nUhgwpr2$Is}DCy zgzN%haWf?&V93m7%vq+v$G7qzTy?;$%5#Fk%(M*gh>`14+9Co#jwDrqHWJK5CX#U@l2j9`Gddt zoSBB;08M>jOr=YfsB`vQu*^(n2pDuAK8ZF_^+5l6vYrltgEL^}m+;^I{=O9%B^Wx$ z7lTqFc*77S6wgSE`aGz#AVYp8i_Z&+^L4N;bD04ZB&C=fozQVm2k+SoX*t|t-m0?d zav2{OH-a@gzUVoCkd@Sy$mH;Z^mX*x^)1U=RheYvW=ZD>HsyBb(q9NyWYUM({NkKq z^9XbgbX!XJ^}vcrmi7ti5_u{qApBSLq{<2#&hm_ee$i$C62wNqByEWT-j)2kT$4-U z)r0Mk3Yf?t1g@h4((BgCP%e_45c0?6=-a+Lseworr^Vk43#4Ri;mF>IEwzS)<$GZKN z8>~KjcEJblyGxEAIbi2|AU`~tPxtOgRy^E$=dJS4{dcNPkCinHn?$@=N{zhT#9q53siW(e4?esZ0Xz@zf)7rcCdWDzDA)y|9?ZjDy-G6C8ZPZeu$ z#>XcuIZZJ8A9^}ox^@7$LPo;R#`qRRv@mRu0}t-am`TwoBlp6inXYUF!Fb7XiKtDh z*BcQ{yntZybab@qzpC3*z8MeUn%SAzMrODK_6$i+K)M9W!xOD7*L=lg6N~`$+|hDO zs>sl%WPogegm_I3W8CN2XOm+j%js3}e&{pb}Wi#a>T% zY6xE>10t#Zr!!_`ZerGml_y(xwT`Q?{Kak}95Tg_OesPZVCQ$~qT*7Y1vp+WjWPS}xmvDM9LX#(YxG zv-T|B=EJG(ixtkwXZx4U%*we}3~M_$lGV{MB!}*Q*hu0+Dt)a_>`$NC9Qe$99yyuw zQl`Xq=Y{V2q+-vQtJCWg$;9*l?JKOyRy{mjYvg}uCR_4n++;{bj$5LFPR`3yexdrg zG$Vfr(Ohe=&TVLZLZ$~sWlYCD!Uahr_u&tH$ZQF`iT|l&`q~%1U_th9Mxbwq;F{IG zlcDB>C*FPSC*;)8117&8{^Y-|AIUwJ5S?Q1j&H%!N^bLS&*T%AX$LAU;x2G zn2zkzkMP3cc?%NXR1jq(CPGko;koq2HGphYHC4&e9XYo&-i#kWwCx} z12EnJ`bxT!YJ`3uTaY;RuF6!IrQdrnj^rz$i6VK_4`rhcAWzI>yX<0lMwIvtxnq!H ztn!AnJ6wH`6>m~y#U(9Rn#LLf8lYP}_)R#4d$64dVJ~`W|9Li#b06>hwp>3?y^2QYFBS=($UcpO`oW&crK)AjT zcdpNUqBboCO!W9kL*J&<50iGt9tP}6l3<`0nS|)+iSMOfZ$x>scs4%sAGe%Z+;5FB zfqsFk^PY1Fi{<2%5-g97mPh68yMH6wpZOV+g(!GJ+tbnF5=hiEnlU@r7D#{Yq5ZVK zuh%lfHSfC6Lc*fFI~C>S%e*sDfK2ptcUUq9bYI=(JyKoYXy4(9U@Z=MhVVk{E|wDf z>AT)0zxv597yf2K2=4`{#NYC|H`{OUj`115Xg}52ZqI~_hdo$fP#Q&|eKG==-Dni>ZW9<~rr!XhuY z4ZeUZ}o77k-TgswA^QGRjI+m*iVm<*#r4ovpX`;CVJTHm@sz^1x;u zA0yM>l#$zD`gK5}o*X6~bX}Q*hwb~O?PuHX_uun-`@6EdLG6Ov1#;>= zsCdZ%?a|YIQa<$eU)C{kjZ(PJ%`H`m3~VSiuD70EACO<3w4Dz` zf6E>t#kN~Hozr)M^~Rt5*`JxAPd#6E-E|8y#eB}-Ks#UnkkEqlPOL2=sRaXFBBPe1 z5~Mgk9iY6FhnXPOF_B1v7}lTv{O9&eEFQ!a8_vG>z(afGz>x!T^7u&^7#^^WvXY5= z>IoT4x6M$FEJA1D;+X4U>;_D$S9y<>zCJP#+91)McuKiM0y+mSeSW z&*G4*Ut{QEz@gmC$X1d=0*kr%S(}(36+2l0k%_EaedcL>ujfR0oaR)w%*&i)C`-vN z&v!%&eS=}A!;6IqmXr2A&%$s>qA}PrB$XR0C3{ZEjb1~m)xIMnPcrAt^Gsnm2Bdb|pT;Lyq*)?n@G1jJvW1`-?|Z;tNu^`};lm;Qczj zO&LiY=&Ju691*cXk&7bL_0!FyozjVVzj?Ge?zgTME1*R^+cVALWnVg0~ooa^raiHDr=e1u#&JEquVDAnXEtT!Vo=D@&E z+GES<0MF~If$en`LECsaUuq+L|8sv9d$%$am)1&|+IuFHB#H>FkyOP=LJN|xkTGvebror|Js>*c!=q~J(VmOx)8k>FuUm>rDx|!s z#wHvll-P8hIw~C}4yxf?DlfhE74qVjy;RQLzEd_dG|IpI$G^)^|BxjDBUJEXAN!bm z{_~%=YyJ3#Ke7qLLY28&Zn?$2C%Wk2ea^E+#%CrEcwSrEq|bt!(U*eyme0s07)*?T zzKNH*y|3+vO-#^V*$)d(9U-`68T45RlVWC4O3EvhnDt3-SG(%4Avx=^r>ai9Q27>_ zBC(gIb)4*x;)+hGXeyDi+Dw)ADml?QB%l4lKg&fIo-beemw%T_F8*U3qt4Uc?2)3v z9a5ONNvi61%E1#I^4s6uZhi8XzrNmVJajmHkHitIsk-RWOKcolEvPSae1Bh`O$6id zBh5RB$2dr*KRP;YNiW#m!-u!$s7}e89+fH8nWO!kmPDklQ3w_ZE$|p5ngv{IP#!!^ z4?p~{Dw><+yWjneeC9KsmJk2chb)=KHP<{-p7N9@TT%@ECg}w(_W%6P|Fjh6to4F? zg^4&qoPp7#hA_bRCDe=Xt#p6_qZ6;h$hUPgDqG1G(4S85JQ#702bf_dhMw>uFO$hY z=CXL9L+Jt~<&RWAUEzhH5Ahz1D*@{6=$78rUNt6T^{Ao&2}_iTbb55gl3@(C4%#aT zFaYvTT6Z=aCY5hA7Ycx4b_eH%xAPro~KFn7m*9kdnRP7H+M z!C`l(F~UGYA_dmhrJ^1ofsl+u-YE+kiIH>2QXmZzoWQKYmTPtKE z4BFRwK$0OSpI+Wpg5Yn1+3#orNEBgRkUZ)!44;^Zu_0B2$4TzVIy|obz-pe790nv_E-ZaEu%~0Qb zwp@1EGv(l~|6(B-h6nwLa6!N(#2({Ay+)>@Fg@zhaPwO{RzdW&_m~mSdaXPiYm$Kw zu7&foa`3?W(>bxWIGvBd6R7z4;z^d@4n|A3p2xOL&Akz?caq-^dw#ul5Tk59rM7OG|=Os zCkQjF-*%p@UkmOt=229{Y5rk_ll>=@9%v`ww-smG;BM^j&dbv*ph=o z6LN50pNx!FDX}on+m9J)A3sOL2w^J_lVqrSdGUf#38w?UGvA( zsM;?5?G|Q<7muUk$V0JYEMsa`&R7Bv!cEPasM7boxibn}L zHV4^TnVrZ^v2gx({)qZ)*`cKjXBM1O{fftFEr0?8%1O(u-tTlH*x1aLu<O~1QI z9(Z7{%_7jDQYJZCwGb=|frt?-4^Onr8;~LhVOi-Q2zj>|Tr~*K6hQb^GkK5-=vVQ8 zVNdp_`uaLE!V7aXPe<3erT1T))PzQ2SMZ4(xa_AQl+8 zld--!szi$Qyd)DCQ9?-lP*2nm{h824>L{qAqCgt(yu9Rk(Roznt!zOs`IFM~2gFQ{c+WJOm+L-!YILYcM4CI4wvNod{|+7skBS3 zrUq3J3@QmMvkb(n-GLmS8%U%vM1Js- zpV;ppK^#6V z0G_P!w*2QrUdYPXk^V7%CIvMrKBc! zgkc-Lg^bSZ9Q`cC{jpL3eG~V(9f>T^6@p8$zKup^KOtsj2!+8FO&xlJi^Z43i4s%J z3>-?7SpoKzBGirzZ=dv}C)w{;0)(9mgRgkSD=fPTdr0`t=|H%9fBfSg+g?zQJo2#3 zsC&4KKJ4A(Qfp%cBU1#zd4=7f@LHU^gWVF1ZY>DyJ->HWDNy6q< zR!$>PkgkatBVAe3S*q{Y!=ul5SK}Tc zkAcFb*ggagw3PDUgMGKq%c%n0 zjl81&P8>a8Sq`JX7({(p)<8Yqedg8!B?903bHd&z^erKhjBzBILY~UYmy@lzh;3Uu z_@3Vd+mbQWuj^xSgJ<)g?TN_1Q_tiPV=f+4eiMe9I4INc@61`NOI1dBv;1ckwioez z7{?CX8GE;S7}2`o`9`!bMm@nIDF#MuxsD&LhaMEty0exD&sKKCxK6?fyfk=sf}RiB z3_w)ybR`1>j=qIaOnI!*62N=8cqOFpkvo4UxBc?_a&-UwCO;4gK!_lKL_nSZn;b)U z*#3oN8hCZch)9NL&VxAId*4ISv~`1Q-MH1pB%dChBPw+}VQwDCC**;1`5niU>MBSP zgb1+K>7yV0sMQ%>v8%7T%03T+2q-*EfAz7yk)ualdV3N-yyn%fHqX?D{^BpqlLvT? zpoh(adK!pY6-)+lqRtEUsFx9Z5D)keRK0uWyMLD(s2!pFuu@-mKs~ zsgG0p?$)}@awJL#)lhefd9U&d+3H30M~YEN$OvQ!FV^_z&@vL$*>=*_zyZh`p|5xp zNj$K{lGakF=>XH9}F-!EH=Nt z*W9B%G=sqePZr*#m69zwExG@3y|Rfe<@5tokW2-EV!+6vjamCZ2oLZc4B*z}Bwv#U zY04_dw6Loni5Tn~v>ELsM{j>hM2oT!UKEveDiwGm0Ah2dfI3enTrfOA5%qWQHC5n= zAr>6@MgGEo(BS#?GJ2*UGs9&Yb2HJ4&kPw%w%|S)khjv(5)1e9>F)6cB+?J38|WeQ zNoZawE--ZCd4jnX8#r3ujn$?1+~FM{Bi|_AoB=hMdwZm(yT?d}59=dbi{vB_hbh$; zWTXW?rFvUi%fj`7NlelKzr|<|8r$)JC#6qy3mz2O5-bvdXdSTU+zaK(CqL7U3om=3 z8F3_)xb=6xl1CoAFLu@M7UQ zCd281i6;8-{zo3L#27*Ny1~$4O$cy=X;FVC0(DBYdV&=sT7bVvn#Fj>xW|~p7?^Ba z3zmW3P(Jp8WK0TraQ#}hAean6<^jnd7_-SF=e_pmj>-zQa!D=R_H-g%y;F9^Wa|SV zJ#TA3FRj5!$!17ZjqNC z3LgtRllSC!u-=K!2m@nsBpJxw2N}aCMy@fGF`O}|6B&$rk5|Hn6%tli!Z*|yfb7iB&|2(hwqs1Axw_R^7p;(&*dLJ{z&PTnSLeu;Cu<(8U-BdjGJ$^b`#`ac55MIagzu3h8iEIqU&?m<_1Bv>jpQA#dj0FI zEO?0_EbRS>mkR>Leo%mom^mLJNeX-JgPerH^KgCoS5JHU!ZY$2Ub|0!=ASLZl0FF8 zq8{+d@g4Z~H@+bQtSQqr>}@$BgI%rCd+LyM9C^e%QM?D}alBIWeL_ToAcU0f`F({F zcSx~82^HQd@3Be;c&`Y>93JSgkip`*%~HE*_X3IHU}B)GVXJu&$q4DkjC7DzcEOpM z=$S@lgKTrc1R2^;v>17jBq|BA^3~YRR--Xym=h|Wv!QcTo!-Y$TABiTyGxg!@f($<2``&f(+0TB?{(knepKZxC*bJGB zI)DD>e{P>+RJXRe*+9}dV^E^NF)CR&PMt8T&7w>eu~t@8nemzkW~3=;3ML(xot{-C zI$(7Z1~OMsU19A`*j7CF&>!D6D*?}sH*uvzZLLvNRjKtkXi8~R1Rxaji{arrW#)_I5L6;QK`I=kXc^zC*_oMn|f0#Mu9a@)qj2I2a3y zgy5tq=3ImEk})yt&58#+=f&DKi~)pVvUV&A!RKbuvbv^9jvqU|Y#anUgO`yYASZ;& zlBE(kV2sRD-7uqM?eWYemhk2A3!fW1YxN+#`z^oymTY^*=NFCO7^qu91py=`2%o);&3rNRgPRch)a^(1-fAR> zwMs?=@K&haVmw;-TY(3nY1c(djA01Na9_6!^|VUE#%-}rmxA9B-WV>oA$Ff3B>-E_ zyVB$mvIE(`LxdNL@3G;08hE@$gzaG$`HX~X1<2ILKmKuB{{;5#y;pwm^Ius?`uD&0 zJ+=`qgo?6|e2kRzf1@_$SHAKUt6M?>KdVIOuk{-5Qu`0@8{`SnM|2Z5C}TPiPhb3s zlCv-TtGxM5uQjjS#pj(Tg-YByb*u|#4Lo_`guN0XIXO9}gnB^Q4nJfBhIRuGnv(Kr zt-~zI%2BygyRfsp#kLv1b3ZpbBMsZmwG5NN)b`XhPKUhAIm^(P*1lfae%R+eB|YQ{ z&*`k{`o`_&CEar@8D8y*vV+I&XnFH3&_dFZ3;BNMw6z`Lk5r=_&QlH(F z+my>_#euiRr4deCP{h`F@SG?z!lS4I1|gI<&qdM$ z+ST7P3RyX{;bMXdFs=sM;J{E^dBIpMVq-~tkL<)-GeMnEzSVdqSd0t#56^f$USKBB zdkoJ6V6@<^VOETOKs~uWfoF4(be*Z7oMiSxUOjO|*&;U>SwU}CB0$DjTSUlF5Y!0< zDt%xza=2C~DJ@aGG-l7346I9;P$QP?gF)0c)Jw7IM`rp5Rj&}{XMM7Wbj8^zlbImU zjc)x-)-uw&dd!=F6n>6)pq~T3+88SIGq~l_lp4%@2y38T!mejvcoNbe55vI;G@M z2@*CNS@5=R-!Avvcc0mnfV!t%IUl|Kt6%*}o_O`u7Uqa&i}h9ggM)I(C6}0&jB$?s zLLPv;*$jE&OthXP6%;6GL~d5PW@jy7eM#k$f&WYbeS?9Ufeu5P{)3^Mh+LmZV6p~d zjXuw_AoCm;)#w18ClUNQO1!#30|wA$**ST(57XmaAN0wpO7{9=5+qvbdq6NTVo4kk z)Z1JGqm?89gM%)UAK+SrMNCLQMwX}0P6gBtVL#}SwX7?iwW_j0b#&rX^+BN9DF+^U zWB|`nD#Ua(C@wBifzh#Kh3n5eT-B%Q#j=0xJ)nK#0V0K55Ozpn3iKjb%Lw6iNPQsZ zKH4?3jEvcoGo9ZuaYqO#p-K5>a53gOnOXYW7;8)h&Sj~=Jv?bdDQd!u-}v#IJ{tr^ zBKIVu4q0G);(li;d=pX)v=Mc*5CX|$l9DQ`BR!EigJ@Yuv;O_2*S*%xtJv@a>FYTu z-N$ZO7ot^Af7O!EK;$9Hs2%F>k(#D0)^BTK`B$KT>ng6Sx3&BE<#nd#!{D=m64ej3_Qs_|XZ=&lk^Qz#h;l_4?jD?vj`u0p z(TP=RuN^&f)MRH!jeF{U(~iwbO0JZ`s#0lhX_sd_;~BQ0F?(|HJcU~Bm4bVY&5TP~ zX@z|I+uycpcB@^6ZN`|yQj&lC$A7f-^h8y>=RNN+Imc1(%x6ARwr<&C{pkO^|AUqy ze{6I>p8LEP$P54Ixw7N@J<`|JV|pL2UT^C$Idyoy96$W1oH~9$+D{y^wC9kgX{`$g z)a+QlG;i7=9o=0P8Gta(Xq!N&n7H<2YE0{wwDW|Uv&pe7-PQ5fA;U;mT|={d#)N2p zZ>JG1#u&WJ9%D+FXydWLZfm=sw-Vs7+iYDRBnnboTGgQPQLF7=X62wv;r_o; zkbL7ex7@w{B#VnXyk?cu!wi%yRc9N5k6=z2qWP^PE>H637hqd?jJ!p{4>0DEdA0C9FbJ?lAPi(Yzd(t@3`s8@Ur!c& zf_}*}qLX=#-tf9C2>KcA$oJ?t4?Nj;t4Krs2_|y!okoRE4fBI zpeAU)blL!2MBOm(GWx7{`!PW^xXMuA^ud#*4m^(RwN|D?iw)ix^L$_Npqrq3TeO5a;wQ@K@X7B8vZ*n-f&eO8E@Ouv?3X!QkHdWPfVn=W;Z5cJ@GIk*+ zc%Xyexsgfih-}pb5WGasc%}fGhi!B`$)eWW#BofPwLb75nQqhfuYUPU+~#13HRedC7sGpOpx08uS2k>xJiRxG+Y;@+g}1G&c7t6545?n+ z(%8I6FchPMz0!VYpBf};Qw|Tvl~1@@3e>KstgSKItW`-Dp1F~MKB=l%x_$`#%cT0~ zz<~5g;^=;9Km3>x)f120DcM@L*das);5mYP zazJt+Po6-rnR#W?CMy?p00HW2JEmhwu3EeqMtaz@Y+T9Fr1m8U&=6sp`ur9Frk`_Q zAD1X`HmH0;Xp73Ll~fH_hE)%)j{+hI8vp?R^hrcPR6` zWqp(W!pbr;!|WqrdlS}Z%&cQ@^EXPF?>u?pSj~Wr0*2*teXeB+YuWQSL~}4gG<-cQ z2lWQ&!Nm)qI;tInO^H!LB&&ct;jVS}jSo+@AXgle6+`KBpZlz3y&(i-$Bymtwzs{_ zG8-Zk%!)wjs4t%an-1@0^a$HW%3=NcK) z*HH@ri4sD^9_ukhyph1TNfc&aUxzfNyvqmCwSS9ZPVOnQgK2EA{$NM>Zj*G8AvTSP9S@JmX42 z8ObKcz?^Eh;YvW*9dh<~gaW%^BhKIx0pBOcus5zoqfc_=;l{D}*F*n{s&2;Lf|5Mb z<=VB90)pkj3zuHo^{sL1i|Bpl31!pqJT>ao;FB?#7-D=`{2gN<{WWUbXI$m;uu;x) zGx3Uiq6f}YJsbpOCF;YIEE570QMbXo z8-tx{XHBvBzS?MNiw^bos`R;u7Z1E`Y?SOl-DdH%+9%$i?@&Y?l@#evpB#)?-psy+~T7M+Yp7&UuZ-wNC2n zyvmw7yT6gzcm}J2aNZKFMJeX~bdf=4;<%{uwkW&t>t&eT@arbnvznO`PeZ=1xc+~=b z2haDwV z;X-$!+JSm3C_!e5ZS55sNHT-MhOLAaLb4LIA7q4j8dlj@EvQef<=ge~1Oq$$=}aar z7$_Ku-hij>Fv4i-KQthR!4SeIuNHF7vk@v71%Ai=LiDX5S$RAF$zZYo`QbSr9Ow`9 z7djSMXF$YIW#WpmyY-}Q0*!OnI2au@G_<^LK~8hE-MJohwo;(OA$*i)tqfVOqcM`x zmBpNF9pm)3Vb9%KqN1`whAp|q%E+l7YgdN!f9DB1v;25QgY85y;;Bm3-)56VHkRaw zNjk;^%MfhslBg^^BNvW3M~x^g1G24li?(%v$ue~^tomqrVtKuyo1C4n{5vnhoD&3} zW5eLFM?XB?eL5NF^Ymj+vglhjj;Q_$P72Xpkg=D%;E$A`^vT2+A#?fG2a9X=$i#G} z9NqhEY1(w2>b}%%jCjb+D>9;W_b)#oMYZQiZlMdc3t~u()N(bvr&LA=Ve=p(9$uS~ zo>Nj;+vE)X5Ga4Y5~Q)g9(~qqwIfN~;&gU8fN`#C-+;-8M|aC%XCs-7r_fhlZag*>b})q^IqK5vI=LkD1MsF-OMKHmRy4G_Fc*upB8>Qp$QLyiSByLP*B6 zk3ecXrsgtj17FU;%amVKDx(8E3*?38#vA3xpN=J=wN9*utEjA(Nj;CU13ZNsgz6Ed zSD^J5Y?G+=$uB~FwGa1q+r1fIhWffJ>~I1i7w~TOtBj+IAcIjMgPx#q-_^qO!^D`3 zj|^JfkM(!TP*3|3IVjL){oxOPyp|+uMjMcda$YBAoaXD1V?xfNaNO3!L>rkZa2=TY z;(4MF4Cdx}8pnM|*xrN*G(wA^W&*7b2YIqWYo(!FnPFz&cZbLF2lAloQ4btMaf7n& z+>Dg76jDAKm_d>;JO^wU;ir$5yXQj~7-~JAH*0Y{%1`g+Z@rz%KdAOVrhKGXd zj*N|13@zt*gP&}2tWz^1R1H%WNCyX?g$#nzbCxwQ3S5uH<`}lD`Jg&{dD8)dD`oa& zTMZzRc(Rbobd9sM2Fi;1UyJ8jZ_xJW2l_e({USK1|Dk}sNWVt@sdr@2Pe@c$kXf-% z`(1ya&o2kMFDsk)^T#jbl<^CL3mp*#{U5>wQAGFRfkw{Asz$qc0~)f(z>T5iiB}Lz z66F`<2^+!NLh}{E8NO<`DA^+bwLnV0tZAjhV;Qu3tocgUBSqUI!|O$|kRh(W()FXX z732e%OjA`F{O|Rh`ydIjve<*FRON*Uw{d&VnC~Vx<6npS6jkUL8Cpj>uYcwj<*Ll6 ze%1fum4z_>pf@TC$PS@=m6g?|yZIYWOfYk85MHM)B^--mGqTUK1r0#XwPz_Rv~hpU z$B^3ZQTEtv(Cl3%jHwgSKlg?A_?Cr?VM)fm_32xb**Kh-K(DkDQm zdP-xHyJ0{M2qUE3##BxRI#0=9cbk!VymmoPR5EZbUM)xnBq6V;L?+a@ri>WaWpx`Z z9MJ=rqE82b$p?TK5fbR-JVQHWU;7CoSbSDkTBXlCut1{l zszKN?vhx-aMwHex8(E~DippzcR{IZQrYBh|fv)+hrim{qE6dcFa7iODsPQ~Oyx5}z zrVC3S)Xw8leUI6ojv+>eCu{)4g`omra)n8BEP`@`KTC#WzbU9xVJyy(so^Q}o}>cG zjfnVrHI{?qs9!Ghbi(+Se={s1K7P zbv>Pv0cTbREZWP(;?L_nm^Cccdr*#9^FVP8$^e2H1X1`Xx-eKHLT8K;#B@g)AP2V?{zg*qf*3Ud92KoFzg4Q=!j{hT$8%$P&IlX*-b=|0>n z9hs!B1@*{kfG&UpC7XHYe%{N)fv)zbuVeG+JnO^p49g-lZV21v8mj^N#b(jhaD5h& zawWw@_FU6Cpi} z>C_S1wQ!wyeVjE|^I1B^M~F^#PL@oL>ho*aDL>B@BwLPP@h*8esS=3a=)|ZLC|O|K z@e>76K+YH=ef=-isRPf$_nhZ}d=t`2$RzUlc#jQHAW;_2!Z;s%e(I5RbzT=iI@siu z*Vnw_rIzWB+2xg?~Ie0Y)5(83RkDpjv4@j}N%CHG+b>^XT*rpAZG615d8 z;j5^!C@<{3OeN0j1GR)W24om=LdYX!WDLR^jX}A^QdPe}rZe-Taq~{QKBSa2mmVYzNfa{37!FAc z5}_4C`wmGyXa~p~&rdjFVQIyZ=Ur2h#f9j|7#%33DQ#5NMKKdOFK!k);vUNo(r-!O z6k-Y&4j|EQa*J}Mup-|!#hsZTe}3x96M>*2nek=zU=iHQ4-!Eveh>^5qKH22Wt74>(}Au-wwszaN}y0|SA{C%3ghG+Cg2e8@E6dX<$G z+RuuN9J%mBbgrwWubd5N0->wk<9$G#SF+BO4JpVJUy3~wQl40QeBgqGZth42q)YNa1vf9m(SGmW$ zPfeT8*WWA+r(7vm?=?HtCMO^Hl~nD##*9_~$qIUR*yNUk5=a6@ZZeX!1S%7RwJ}jf z2%x{mY7izDiGa=->~6KSSsu2Yt7)t4(-f851Kj}FtbJZ>=@R`ddc_)8!TMM8po9no zAQRy5K(A%q!~4T;W@e^jQ13aSWFsRZ%Qj*l435Iv!cBeBhzjz{q>Lnq3S-4l~h(0$vFI!3)3uS6(QnGTgWNK_m6-|0& zw;9+hx35-uv$^g*)Rzl?TnZkM=fqoua^Jagmvulw2cP=Xr^zN2M2x<^J{P*=>SQ_m z=MW?ILSR$5UAuPMAVa(T=7t;OTi^UfEG@em5D{vGoM&SbQ?I-^pX(7e#TqLJ9@p_n zCjemxET}~!kQKL1E+`|o_Hkh^HBu=i2fyK9rWBIr4HpJ-j88%fA$_D3CM?U=$mpec zPdPFAIG4eY-vtkp8s|}u&dANPvU5$6s_-lfsw}7`iutX86FIdC*c$3yt!GuJ0b(MLkX?HecC3yss8=ib~ zAU{zsInv*GkG%)7Q&CZFG6lgx-Z8Z4-*|<4db-v4X3;Nl#wM&~QfmD${m@Ics30AC zI^-eS%B~UOKc3YBg z(5{q?YhuKsYxtWmN8Up;IG;?L)Vnu!39CkbvYr?jP)(<8@6suwdr}=YsM;vv0VzpW z?V!_;KgyB_WECV@A*%i`Bp@5ypU4Kz2rfKmDoxtbcqvl9DY8C=;?DIaZ{ zU6d&ULjzJ?SSdr=Pnqz8bP;k!JH*pFzHP(u?Z-KOOTP>DbFM}7htmtIm(R6=pP<1e z0O4n%t#}@ujrR~O?W(Bc9-dGTCWtr68}-N?#MdIh)b3&;h;?Y# zC_dbhkk97b7b}U|Vh7$Q!uUuuflbbuqH=v--Mr1bQ)@}E00fY0B4_E20}!m;yLa1j zojmfGbe}wA?L9rJ_N}%T9yIg@;ekQ#AN4Y>q>~Un)`cy3CarJwQ6uXlzh4b_o`KGm zCFGW{PZgpi1TGoL%BZhzj8uD0A!nG;OUJCMYf?e=Mz#Jk?}&mY4Dpp`G@LR`4dwyL z(0hbpJ1rw1Zgsp-3=9ws&fT+Tk9ohyR)|3WXnc+z{NM-j z-S2+Kh#KvRQOs;417%Kbo(@?3s(7bN5w^CrSm(iLuB)q)&aMts0%TcqGnp6)CRqAk zNm;Shg?lJc-TSu5&&juIFlci>4$7L5nPZF1rVJ?h_iYS3_Pj95SLQqOa0xNv_I7~mOwC*l0rx#a>%xqKT@ zR@#K~eVH+^_>Si@1*gU%e>)>&%ASLJBmx;9Y0vb?`F_`m>(epcr(fYi_yK`*@<&&Z~R_sNwPodD^<6OUfQ*qJ6O16`tEuOSRk z_6DR4I3Hc<))(`8EDlPyI3S#s;V8kT86C6M1(0_ffwO zgR*)sl*~owHxEM4@G2%F6x^Tee&tGROy^~h$#!|TC2MbIX9&GXZNeqdvr~U;Juoyn zWY3h7ohO4jmicp;D4_2#=I{)O>V|aCi4a^PM4Rz926Io(nW4|BYpTuY^XvRrHyVcb ztfQ~UFv#nTH{2+b<0IA=@Ord7wpYs8h*v>c^SVYhX?0{|X7rRCyXm8H)zjas#^&-2 zf2+aZOqTR0(W>70^d)tD^27Vjm0;GwZw+ZzloR2n12fo3{ zBKY;Mf8B@~iWZ}g0g{dk0bo`P@Eo)`ikX>e>VdTFxK3MJPT4sSE{s#!5Q4$1Hg)Cy z21-37LkB)S19-pyv-R8$V+Dhfn175S3|I)C4c2Pp@H za#P>Xr0qYgouw=GNqo=*^a<*b>tf*YH{~Z8h_4ePc1j|7?>e87GbNYY$3G_|(iv~W z{Lfnk$?onR`#V{g&tyPgATP|c5=uz;S-SmoJ>a|?E~R;p95Vw+IfDqx4etpjqwQ#K z`lAN}BVI^P_+5Lt^F8=I_X&C_k#mSjRaLdg6?=}b{)=-VJ$My}%O?Z^0vHd@L&B_I zV#6^EWY=%aQLTd&eX+g0O+T}bmVsvn=m0{Sx$jy5R}!PdNsVs8 zZ~)}VdEGNjA3-E}eqPZ>%yuU-%ELglB_csw3-U<+hI|CiMP7VHvHk(}k#Xe#sSvCn z5|uf8E#7-XUEjP!S|>y^Bzrz!*V7ZrtIH&3o&;E#GNwdiVroLi0OwK7E6A1c{&DRO z_GilQWe1caBP&BHD=KATTKka7M{Px|boHyOj4dB02-(cdWxq!}7!T+$ z`Z_4Dtdb%OJ0&&rUv0cnos7PLWPw@j7p;dL)V8aW zdDUBk{k>ARWsjVAO3Yh#co0fgWP7%W55W8OjWDB8V}SLgpI@wpr}OqVR5-k zPmD`ZS*7H~2+~S${NX#KS`BbdlnB*>3=vWXwgkc=*~bcaasojMmwhb>!+HRc#j~YL zR{32)QHjdzfDE=BTGn2^-XL&{p$(0VONjXW_untoRaFZlh;}FQA)YlNE$X+Omr|MN z`-5FdU}q++?h8uDjOYjy-yOOCW~po5EaS2Mjw52Ar^D_stNOaErdi(c7av()lI4w% zSbsF6V$^!$8w3qv4_`*En_vt{83knplB1+*c4AQ>=a%HiP%Bx9oRW<)&^_Xnf%;)F zFoRMeWu6SwN7Q`*J%*)o(O>^UXIeG57!mCyR$6 zOaOx)ASjR+W`cP>NESu{hB5{N-WI%f<~`D(l(Nsvt5Ku|4C(7f2S#o09l!nn;lneQ zOeT`S8?r8ZYsv=5RFH~1V(>7d)ZN{+#N)71P$#~V^Bw|5xTNb-F6s>IS0>OPAt5D!H4$X)f+$51MRjJePpe{a}z4(2|V(2x{VXM2a^eeh7ztvnWou9 zd3@4H6|$rsY5hg_zo>epedr&^6n&94gH$pBfX4%p9F#YR7v3%Ufd}#ixq^gJ2ELY$#b%Mkt2tKc!pJ1LMKCg9nPatTU#S#D&LdSlQONwi@lDf$*=;*&drpJ9GC5oVP<4}NXiN;%yTx- zwdlnPgYk`iKdyZ}JSi81m+N>y-syt?Lyu$_Y)T&PgQpWr@UTuSI4QWylcnSHgiR*6 zXM<$X&socjG1_uquMsRfjRSpMQc$V}X8lIkJw6hu&WF@E$v3X{?G1Z~gEF~GA2VaFRm-@`bj?(mNJK4m09i4~t?ETb#{ z*O?-Li=OAnGHvNSE65Pn!|*)aeG?f)J}CohknmovH}C=hUw8B)l;G&-h{;b7UT67N z|5>o#MZt#<(iV6ixfku`y-D84_t$af)tJ7ZKDZZKC4^H?(@yLw$OgG{N(kLvd5+ZK z@q(P;FrL@%@W$cgB0Q5dWDL%bau&0bo}K4q@InVbHqcMbYqNCS9}CYh9tV}wp<|FO zCLDM^h+U8z)Bm?`-yTcEvvT7(3J`;)rVV!8wl+6=y%G@mMfp2d*CWLPVb<*$Pm ztDvArWxh+>I=+(Ud@f}kR&8^)rOEV?I!}fuk`=Ff@j$!cNo2y%BOXx;0+Ka1F{{tH zXyhZ`o3?I}A|;SxbHg$*GA&d3Tr<;JF)8Yw?0oj;%2i`k$-%sveCQhJG=epyvOGR9 zE;AGH$q`^;fw6-A%-Y9zVX%;}V1EdMd;7`Fs;Ua@!}&G=l1>|~B_Qvt_Y00yVUjhf z&+GMCo|2I4*o4Y&e*J4BSVZCADd=iFE+uMcVq8KrKwrm68R~A8nud)FL~E$^q@23% zW9v$^!obUw)mozMoFTnia->lz$NpYt2VwMwL*9@ZLggS? zp40-oBajAL_o{XQWEhfmCIZ>3tE*Ik(PtCXsRG^397=O@f7=|cvSkVORGKa zS_6GkU0q{kVIW=!cwQabRHt<=v_mqiHOZ>3t`s(OPBzKq&-d4(z3~JQ?npZ7C@)wL z{2n3{CR%O`ci)8(^*J&?od?NAyk`s|w>(RFPWD?L4`S;M7aD~(i?vgEDz8)XO4yaq zunu}&Wi-gqxfi-CzbMbv^5SLn-mn=pHbG@cwTw@V%h;^S`iRPSib<>JBgixvk>jnY zV)7+XU87%J2Hq@NXGfX(bUaE#&ej8X)o81Z&Wu=d+ap`h}QT}uZ41P-jH6us-|9!{Pr*9vdf++_04C;&OCkSXls?@ zhi6k6HOichV^c?uE5RIC@N!M*n1eBm_aPqX3a{e|0;3$S z6rp5^!U@A`sxa7#>o%L+g4brpp2hlVYi+fZ>X6(cdvBGx4O5di|NYga=kDp(?b|hz;|AuB$_3Hz3)>UPVi1%)S$%qvGtkeTBZ&>0^?(oGjH=(20NJEHd4)8gpdcN*_l7o} zN`P{ppHoL1Ug1N)XiE;hM;0Im%tmn?-_G&Y&@}15$30m)Kshi{Ec8Z=|Ng#iZO^qc zP$)B@O7umHMcObdhd~7UyYJuh2MqLd9W?B-h7m{o9655->W~mw`U_d;9 zkukl8%fePwU1i~dYq=&X2;_tV5Bf?ypOBpKiD^qNkqSK7Nfn6*2wQvwSewJS=QMuhXo}6e!d9geYop{ecy7vtIU3dG^=mlhH$Byl$ zGse^)8=rLHdb!yu!@2n~o#BY*)WoD3{@IeH|1n`OHLLO`6EZiIA=9&q<90!AkrZlO zK)N7Ub5rv&J5HE%>UE2LFrqaxD`O)|$)rcvYa0b78JJw)T7E|Ubb7Qb^j>uq1TTms zBuk%zy*#yoVxKJHJOBPUsjg{OLp0xJ<9(Q*$6))>h484lMmB^Uwge1~N(5MQH>;%g zv0uDn-PUNO3cOq=4&5v#4m>2;H9MuI{!-aoafy_aHA(iS8p+i0jM&(!6=QO%Ae`7+xo?0int(sWhSl!buYFsx!V{nn6u65gasuxx_0?T}2U zPR3j3+k()*qP!S6?d_Ixu6XK_Yf}z%%EnEL_3PKPbsTv}$|`FmM|Dlhv4hgQ>p~-0 zo(RrQkK1}XzwQc8VZ+94_FIpdxwpzo-}#UnJteYrL+ry8F*?77Bx`m~2idsFeDz+J z;Fv`jgMWL&AP8?@2aP2UJ}aomwODSS=pqbA*1$2hn4I78n|MZhgsw99GWzckKCsdm zSim*o-EXadVUKqTj}QYag9gU4TaT3`%^NmJuZ4Jx=|Eg)MimC^GH97|3e`y0x}H|U zd7$6T#stCO?7_X2>U=BCALdHyt8zqtBW5Y&XC=ccHh$o}*;R+NsrnwIFMZ3{{o=AD{XEGy{Ycea2G4BZt zza#S)WEefhz=&rFZyh0$00Pj})ouD>y+Ft(lMbDo9dSu~A~30doI&tYMR0IThA5Kd zi3{FTLargEs|EUxwtx)db)-M>>hG1T%<+g8pYaF~x3Y>#8D+1XP(3FiX@F!DIVG7g z+CL`4{fq0KN-9V%UMQlvG{0C$e4ZLTN~|DN5T%@~JSiUvMMLAoqRhX&@dgVQv~a1y5~*t5ZodQk=GcS3x5fU1v722$ zhPzFR=KzS-z8}9{p7N}>owm@yM3~X$>KxpsgzHurVlt>`gKVj}LiN^#QoLdFf|rZ6 zTq6@5(xj}2Mq7zTe7$zm;96ahoWhcgxe?;+uPpBXS5>IQ=AvG8P-SJy8I zTf>`0G7cXu_jn=baRTFWGdixZwTaf9$~&GU#-Buh?D*suKBN-c0K&S=qonZUl=D5g z0^xe}*%@VG(At9f^PzwM`DUFK_X0%!kioF?)X`;L%ZG2iTt4~lBG-R~^I{*Sh-bfh zElJkwqEQ<}Dh4ddJPdvp5B@&>+EMU7H{(JN?86NH$yS4JHfvd;i!dNreMbjP`EU<^ zz3}Z4J(o9Z{PR;^c!pN9p5SzX8F&mC3=ssHzZo!?-NERuQH6f$Er<%45Tp5n<&qP7HtN)m=gm=X6nonYXmpZNX9 z&QwrFbb`J!`6s-Iy7#)md&NAV_6o0 zFp*wcjz#4eRNjezT1v$bB?}u~VGMVd%p>-Nf_{g0fe?cvMs+m}g#LM=y$LC@ZVb@ri^ngVxvA3-mYE8b)o^;KxMti%l}=vocna>|$0Q(C=q?L`0MXQ z>zu!R>7u=Xm+Pb&)5Gm|E|9L~#vRgF`z$Hnb*}Srbtq}?X_2w%;RVv=Lkn#ZFw<@1 z8(z1w3|h}5&%l89W8U=Wl(e-Tl%1D8&B*XsXD@~d_V%=^eX+QXDeM6QQK`Zo4?vIx z0~1AfvDC!VK3`zt1F}w13h&Je0|M#Ee9h)PGTz@|Yp6(463=@G$ttXKp~gWH2g$nd ztaBtIe{uViaL6EQ>nw3dEK(;55V6M4q`|lT?Q(m4{~PTkEhHmZE7*)U3S2byHUxA# zM>P1+@`KHe`3-Tz>>{6&rv{uV^6Y#a@C)^|fDMI+KGM!DekL36BIjzb$blXD?;rxVl*|n z6>Mv3H5p>VJ_rZT*w@=*Ycu@s2ua~!2AOLx6H7g4GuSKbLVME2Zj<04n>lvZbx9#0 z1e}ARjRC|Wch}s=h5Ic>5)nunczE!xA$NFps1KfzP)tY{Wu*KVcE~RSG4DCw^|NFJ zaz-w%slds)y|cgLfl#kfHK?e&!NCD7kLx2IJOkxs3?L~3e>1aK!;(l;K`(n$Rvg{iF+uQ|11>*pFbI*&&^P|FE^QX+2>r_acZzl zTzlHP*lThiD=rbqxXKTEyycmUG3Fs>$>5r3>HR1ZB%6tyM0zJ!_8_wIRm%s)$QLmEPAm}&ldsilsOAzV7*dH$QT7bu&|Hp%GX8oXv=NpI=mB=Ku&+wi1j&{S5Mn2WLzP@P=I%DH-dQndu3A zKDQo*YtpZH=9(SPmqYjdP;D*NYpHRY0&e;BzsP)Mu9R2Q$3BS$-l@q>J=&Ex_K1wN z=$PD*A$Rme+Ye7WdvU@6GEi4jFB`UABHMRAL3V6COLa(*w6xzV_x$w#$Ya0!g%a!$ zwRg5i(T-hGQQst$g=JDQH7O;Nb22v2E1d&9F3VtfrEUJpdTnp;FM-xW_`A6bNNtwn zX?>Px{|aul+}_eExp^+rEg0->Qxdnh;Vs~|Gi*#8L2geMoa|&tvC1?Pz#NQQJgYBz zf9nzHJaJIUYMX82a%$C=P)BIx^6w+#*M^2Zv=|_0e2^ zpFDi%I;qghUUXLM#5C|9Pk4A5KX5?mg-9fN1D&i1ZzN2~(=kG| zvyi9*Fd2jw4npJF->n<<*$};a5MITs>3C@4=e0txo~UoigE7+E8_X!kt7}7!n0gF+ zpFRldgp6clW}9JO!ep7=!$QO2V!2FM7^0GlWL_J7!b0*>V0m017&5+qq9ghN1_ZksY z`49GMK>56FfV|PSkwM1E^70DvHu;dfwF1!`M2nz9qOOZKIucsWEVZ}1tU|JaAi>4d z#E4W>)tPbXL*$xvoUiv&-IQA>1A0zLS*0Zw=%1D+BP~*xq2peRH$kk%j_I>JFeekOYD4wT%gm5F=9IW|+%@6S z>4))zl1c3kDj#v?a6R$bb?()&`;rT!y1rD#rccNtH(w`5es@epdmB{-Hc9z~yQH+F zMjA4*r7|m5rj$5LF!oGh`(#Ki-YT24i;p3#KQ=t>9bD?g0>@9BGy>rP`LJzH0@qYN zAwz?GQd+gRj_Y*eol2x0714G2X#vW5M&qXYhk7mr5x?2*;3p9fyz^M zkLO95q@J6hN;y*-$P}ku%E*%*bCde&6)yRTnepZS5TRKOxKD5wdYkRe(CfQLGI&KA z1*fx?f5+#{R0NI7GZ7e~40djSm@u$Y&gp2kSv~wP=#a!K&3_pTD)1VUPuc) zGvyK`cwjuQ#3*C1b9J6+hHEk)lTq!H48&M7Lrgh?l5rv?%0CYH(|_op908mSxVigJ<;mNd(d!leou0ZLJbmSDM2?E$%hsF2p( zm2o*}iy#>MtnO4#5}JfwsIINE#1cFQvz+Sz?n@mJ76NI;h@~9VIpu(GI8V;<=frbb zZk{DP@Uag=ppJM{_qfZikqD+@V!z?{WCetHMz6)cU)%7tI3Bj_26q9i(ItFA9atB%P zh}SkzaQF7Fu|E@yHNkQdxP~GIB)!=I`KG5bhg{OVe1B$yRiKB@!U&ISCO&$2xERa*~x-SR&(U(BrLQk{W_V@&E`H&)u@`ZmDb9Y+4M0 zRlD^(+efIhylSB_ikdEw_G9nzmY7vCcaP!mR119$YUw zH-CvIjDq8tfjF!skm1?s$t6T-uf*ELG6_U#4FUZ`I9d$5nCDXeK5H6j-F4o-}X+8VCBg5^n-hE!*e z+0%nQ+j{hXG`4k1k@i8zQ*CaMBT#x?SE`4sfy%>MrN32S!SDv2KpA%;@CQzpC<~(n> zlA=EI(697xhJ4VY$14gpnb+s>1~csp$jm4rAar~}u;_@iJ2Pi}eeC_^LbXyEEO_?t z07Oi9n4EEn-ybR+P!5b&2EycU?ScHpYr`n;{4Ba>lTZe(WZ+r2hwn399{k_o!+r1N zXYr`e_P|&@-C>IB0S0yC$$Opv&&X%C_gZDOloS`4w~KWeyfP!tfzswYLPS#mbxqaR_^)J|LZ5c6pYSK?@myiVD^ku%{R6aHxaj=S7hKO@EGaC1^Y8PiF>0n62VRIuvTsgEpOupI)cfw zAW2IX5L3ct(Wmhy;U=|CS$h?JVC02RQ*1GwD@?L@uHwpC85``ewLo~VycdD*a`FqM zr|p=qnQ=*3g>;=dBt<3VQlPQ}AZEQ?ZL;x_mrCc!M`XC`p_GVLesP1`{q_Hlx|T*; z&y*_Sk*q5U7ROCIUUY7JHZFbc=m6BgxDOnA5l83>L zmrMUHJ2D^>+0!ztgk&H_x|pCH(!Z%^*1Ju$O-aM{#dUA=(Gk^M!3n7;Y?--fBPQrq zyhV6eCzQ-o)TNo~PC;^s=~2~@N{EMg+GVh(RR%g+j3f>Bwi{6z>^P}Js>k$rq1yH& zX&Tk`AM8;!p*mg7KPj!Ou~11oQ2yZD0@W`#5jfDtl$*~+h6W^G$Ja1vS^||t>SxnA zmzy5w@9B^+BPdE%W8r&w+V8UTDAh9M4E0OS_@tDLj7zZ&<2Ccyl37+LnSuH%oy?U? zE$i&QmL&vg^0AXLed2^n9X@8qbX%|LCpSTnk&|UdPFaDy-re)R2}#zzBO=dxit2d1 z?Y)nPG}on0veHU?QCOE{9g2(1L5?_YrDOkT21zbJIGy`=KWjvSgAhi|(!Ya)R1jJ{ z%i!x6gBb&zPIIP$jA$ev@cTlM^ns)SJTFQDPZpaS17_@}l^8pNH3_tj;iWVWWDMaA ziWz)-m{hu;EVzh$>gss#Z5bi=Nr91EqzWaKG~>uYSZ>keh#{d>?1K%$GmB9^T zg;!*MW_r@@k71Oq(T69PYq35MPZ`&7q{XGn4~AelvJ_8`K**8PMO}kTQAUm;C0uxh zM|1!prahfrMx3mNlojL8kqw7%IGF#A&w~R9&xaApb_1I>ZPM>%mH4brcr6*+ z+zkG()YR0r($;`z!SdZ z^&bT1#2Jc~fp&+Ka$wwhuc^NuAU!{AxYBmZDatbMRZ#(?X-db^=oIroF!@3oc6209 zWD*5m-yW&dTa@9G2uyAeJ_r#Y1UN{JX0(q7J#dLY-$nM=Ym#+Q?0d%SJsz)g!K4e} z(5!Eb_c%;?P(ISP&v8=dM8;dnbkj{Y#XeaM*3kG3iG#o+(|KsGWo08I6`N2i-@YS+ zknDV9DdhK)_y16~?5@^xk`t$q$3!Mt(i4Qw_b*+)s)yzGlfdaJ%UEx&+8Z#MrQGQ|2^4!U{IHX8$#=KdsMz*NjQT!U-Mor<~kp zP0i}FxwU46Ig6E_nN=k7rF!2iS2rL-$I$5`?ONCE_V?VF)^A~s*1y`L-D|Ve4fx{z z*V&q^H(wk3AOOMz-}QHya@MvvCDS51HtP8wL6^Dj*EG9eZs`iN;u(P4;yfAc88?|K zsw;3rOXUt5eWHI+;bH86urU(6UxN7sivr^l@I(Luo-dW`pWs(`&C^cPRRQnBd!|=v5h}UkZqQ0R?joWGM zK#pi8LLwtN6;N*WR3emMH8Y-P3LxzC0iI=YN)@9TLnt6eteC})Klwn|pWC;|^+&)U zA4PT{W;P?nuKyABACw2+`zSJsXNxs7en26e{HPl8lri4KiU)+TQb{D99KWv_1d)(G zG9L!_cj8{*as-h`X8EoD8#9n{?0H#VL5LT1>xgB1K@S_Hps}$*+hi$G@y05MGFZKF z9=unPw(rRBpy`hdn>O3x@U@g(Jl(}g&NH-bJYgl18;Kf_hoPa=j9u!kyu2iC_Po`= zlUQV^Qe}QnbtJ?!9^AU7<#ik+Kf(85q)Qk8o`3oXGtR8H#f!w=XArQKmX_FA>4Ejk zSp_0YJKPC9D=?6JT}L5GexqbI9pIHDxd`$dZqq1_68DKJBNGx# zDxzy3Im4r4Hk+S1VHT;Yuh+iDmH^SAg1~1NoJl~m^lEa~U3bOaCkOXqpP`JL0^8q+ zjfum+xNg{fj%A1~Xm6A9?c3#d-+6kvrxF8wUcUcB_(r? zbrj~3tWNcIUe*GM@&qcUqFknjoE`vHKS!p9re$(?(!vxO2#RVM9kMJb!>kI3Y9e2r2%f(bi+^*OGkIMW1|qX`2nMt8Wah$X866*0HLQi#|9uD$1~cA-bPOnJ z(I>zFvvV`HHp{~ndae)%Faw|uLBvS)b-&U9H3|?p6eLDTvS;`GWw}hG27ZgYq!ONZ zrUK$leYt&?^6i;u55glDtT`}Dk`Wlbhv-n37BZ(sG6aV9KwdrQixrh*+ZxjLS^V4s zxo{q$*awEiKe!fU!kgyX&l#k}W;D~B@xuM^1~E|L0rO?zm1HT@0Y;c7bIAyngDY21 zzbw9Ioto+{C8pF1siG;X53_V65ibV`LHMPEdnS8le*-oV&e8$cd#D%T&*i>)wfV*0 zIPkqIiw7P__Uxfe@ib8=vBxIR-#bCvQ7);m^57d33>I=?KCVMLd zWz`x|StR*DxbBuBnt-^^7RiFEZ{L35x=#nx5lJ%0w#nof9%g6c&8O^@34$b41*N$% zGwJHyCA(1B)2emy7r(qrkoETstf(E_a1yT$_}m?f2|89zuz-bb>A{2#A($Ar{&J8d z`dyG{(chxLHC)&-6P9^;DzOvkT^^Y|&=ayDC=Oi;MNeWmUCOT2>(?y@d#Zpj`Qwd4_n+SVmAnNp&hpgyP6);`r0G)PTR zjZ|;kBUL-LN>O>U6lN7m=BVm`?opXIbjrf_Z0*y@4jF&o;6e!BOdspJqQmz%PA5_c z7kt6f)W*qWGFIfa`&{@SSkpCGe`q8tLkEL7C01hrZx!pnCWj^^OWC&C{VK?dGwNZ3 z2Y5q=R}y+FB;KVgo3WS-u69%ylK_ix*s_=Jh`m_oF|t}49x=&TQu$o7iGYx#&{*$ zQ4}Z_^6vD(^0Ilj_6Po)pj^1`!{y~q>IU6H_CjDDJ(X2nxwortl)k^dD^po%nR%r7 z{c>|1*X{xDz)`Nz<1%P@b0R}{i&5>Oi z{KUwzacofsttIfdxk;)4y?*>>_oOMBWO?wNms{5sPKPYk_ez1h(w}nGQZTuR$r)?a zQoS~1DXhqs$??T@v9(yCWEWg-QHJ@>O<-X7uY}mSYbC0;Mff^ii*bQ|M4c5C7OH_; zW;($49r_dAK>kjq-zW>R%!C0G5+tKQhmeJkiGxIaroO)3ew%D;@FWW`!AYH2wzt^d zzKrRREY|W+?d4T9s#^+N<;FmnNzGlmWw+}494YMVkfLqdWbohtnXIjp`tx5Vy=tWA zR#Zq~U|k91r5}Q|`sP^|u%644F29}&(#2Y?L`gJs^p>Ve>tv+!0cm~gh!k(wDut>W zJ?1B-q^tde6sRp7gpsTgWplB*g)jxzKILTO%lO2It&a-Ue-LCN9PI0f%+|L6ChAo7 zY;)lL!G-kYMxaKAq)_{CmFnG%#kEGDYD*fWx@p5g@`!@!4U#i6wU9i5HBJ);Pc9^n zSRha>?M9&3{28o-JU!aiCbb2Nb+_I~MzVf&oAxhNd3*PZeEB;f?|hZW*4T2nHC>bS zhZZi#I;?@?`tVAuqJX?QqigYd{svJq&;DFrk0{yjd7R^w8Ec0~DzP2|`$|AK8h}(4 z6q2gFPYo`&@ZB3KkZOGjeGb=|*;pAPb|ue1mPxJ?7CdG`ErVo|fhiFfppebp-n9<_ z@?d-_SH*@`hME6lYhk=RC4!fiu$*xZ6s=z$cP0b5$7=Wdh7K_fiaKZA&g`vN!*@`zE$pofu;QNipXlLQ#| zkN}H7bibUa^%eXM!yB(8*9n&^h*Z9h*H5$=^tVe3PT7zpNXYE08VD-0b818r?!;!W zY|hJ?p;X|EO-_U4bBvIrj8K^TRfT(8-@kB;4_ibuwvbtlV z;>5(o)eGT#QJc615v8w^p|-Zx#>mc2))~bwN4!wMMDS!Q-7+OYb#hUvc)-gtljvNcntR8g7JWEhTz?K=bf=nM0#5e zTbLPZfLPm8Qc)B8W)a69zQdvkrY9#PU(3rlm0wUK{k`2PM_J~@z>8JW(<6CW-Xkq1 zq~^lQEP{cFp944jtJLOnENocZ^V^uLBo>)v@F8WVGmI=Ava7QsuW~+Nc;RGDr4(#? zcI+>Q?z`@kDZ=PuV;N{YxL3+5YfVQ*LglCJsiV?Z?AC<_*FX92 zy`thpZ<&Sfk&(Al+m-OW*%6sJ)+e(nL#%c3b?p(Z^ZQ#{<(CI;mH+d!SHwO^4Si~R zwoeYp*_F-~z4hRu+6T(z3pakx{(jRH&y@eX^EdXst*c+IfB(nq{dyx&xZsI4k$WDF z{h=UKaMNNglmCa-WTB)21y6?D5;bx_55nx0y zKqXs~7M7(%2+N!&E$b;OtA$JXU#J6Xo({@WYHa1&nlmK-ebtGhIdvH#ev?Oach@{&P*M(?!LkLeWWeBeoW3a@_7H^%H2lphL zG1**0Jhc}#+`^z7E;Ok`+i6b!hV*u5nd8rzhvALId=z$x6qI zPaTtnA7eGuwV!x4nFQiqsbt8bA2TU~hZ<0SVFNf_QbXOAmytAjYcoNrDIs^-> zHOXSqfMg88a8X+WiQ%lW;?^WaXa37pV=b#l`%hpU7MLDQE>95X>YA#gBsUqHUE=N^ zuP*sMS)W=dkZ(M#UgrgYUNKKqh$zJa*Me-}EicF5)jDFUfx*EctAF%Yt;#q1{-8^e zAsNX6B(1pl=38Qax%EKR_01L{mYt`>uO?tUDtM;$!w+g%bEKkL$H6hThZckjY&rjl zW()bXSc4Djm5$;9Y210XwspH~-?l>zA2}o~xBi_x^_tho;L%JO?Q!)}k&&wdMX_X+ zXUgpHp@rlIL5!d3m&s#C?0w#rZT1@en>9Q^5l!9VhzCd)fOr*b zw6_bGEU%Nink%L4_;IN#t(K{y$7P_naDiy`o;oCXS!Cvpi0;Arq;^tw+X<;JaIuRJ zD995iD=Cj$`xH(dF>&PN67sa#wNH4OKK$*k%1{36uVbIM&l>VdrBdw}@ZGy^m4cc~ z`O$6nNNs+FT)1hgTz}v`=^YxD%g@?j1nuvB`(3$f-y!+>8{aFBww{pJ{rhL-$j82H zWqRW`KPT^c`b*^^9Z%L9iAYv5{Na%-RV-O*K+R4pB}b%;4~F(7LUuupOb)B@H@JM3 zIUf8tYj#@5F!S*B78sM4A!x*5XtH*QjD70`2FMS7@GoWK#!%0W0Rx1=0Iym+VAP=O zgCTqj!YI$+?>LX^5pozbOu)&LC*`01=^vANdjLOO3!8!Lh&k02cf?xoijYJm75=5|$52JoPNHF1P9FQxB-sHrX z85qI-NjW{jB#Xa;@|UPTd!PeZYnG9jBUv$z1fhlW84N23g_nJ<*WA28+S*PfH1_Zs zI|DlUq^Pobcw;jaqn-00i6pS`WF(c}at_ZDMI89fgJ)!x9~n7t;DEIa`)5r}xV^xV z0o_B`)X?B?+FlO&W_5LqwLvm+M*ETQbWq80u>4W=kP4}RIMmkG$egxOmy%cN%Kt2U z%|vyX2>dRQ45bT*EYB8ISI89MZ?qqtDmHL-p|}oY6AZ#{JUla%ynrMu!LynU@lJ$9 z^1oJuG!b|nhzB8};r5A2ijfHD7V6w4rc|E&9+R{I-!T@aG9j4?$wEMHyz#fOKSVl? zJR+qPRYtNhRF_uPFWM34_W9Wv={|W-*w8ze3<0t=Jw0h&EKsHLj~DBBZ?9x&-!H4I zl0JP;--bX=42(z#Ue8vSY$rEg`$O|aX+Pd={c%feiA?T2vOtIy$kkhS%jktmiJwQt zBcqbrc}j}zFIYmnYTo>8Y3X;iVsk}~Ox%C3%*2S@=>)_pTc0yO#u3O)c! z*(W>e8|DA}@LO`rhyKC5Q*Zd%XJt?AM!DnYe)+o>zgDhRlKret|BXE9qCG~Y{_?tS zNY_NaeC>6AZr8i)|9(*J{m5tKtGE0{I%awoJYtu8;=T4c;5S!ZvR(do>l5wo^~8FT zthHT}#VBGV#r>HsGVpe2Uzi%5lKB}If?#0?O7!faDO{Ip%uLS6M1T4r2^60rtfZZ> z<34leL_efAiab!X7^qY}$9mwb-Mi$PXFV(S*K(XVaYFv;uRbj2op+uY89_jd9(?dY z`PR3-CEK=bvjLbg|H~J@AlH5GI(guM2h30=Mf2Tv-z}f{%xB~`H~dwlfyha!k7&B}-O9-3~flXx0Aos`;L~JcVxmf?w-Q8t<3i(7wxlqFSh5B=4 zb4Rc(&}qy2ckE3ZK%`jz0|90x7*EKcEkfs8=gGmthCK2&o+85D@Cdk2#ijQHl*t*1 z%fIu8Dys+I>A&ueBw(V9h4#I@-LW!jiS*T7gI7LWdM!fR_&OfJLLQid@J23^52SIX z9mrrer1eu)R$@dXkxb)VQtLs_XZAk*5JO~ua?&@sPtZ$8Ir+{X7>#%h+}ak(?T?j# zH4|)R>Wu11s|)S}{5}2k{pac2&)4%>fwm|t%uDDMp$>T-)+w?vZ+<=-)KiWb$tujU z^;aJBi^-8xCe<>eL`gVFK@y1j3gGK@QGQ*W+1qsm>!`4cSUW?4sNiH1^@0~URiedj z(UY7%sX9ZY*5ouVH_xt3h#zvE3@PbaZHwSOSra3@lA--iEf*znGg4SuX(5C?Ek|sR zrLm!23n^t^tRToFif&_ntDcIB~18r?Gm@y|c&6}iuuumErnryr*D=n9ged=zx z@s3Wp>CquOZhUZ9%B%9^`rA9@yVrNh3!Ydl*$tJdtL9`%3D(%Y{WAJ5Pm`K0yQL(% zRL8~&QLStDF3i{{HTlI-(pV)^n|8?1l}g^8yG8OYKTArsZjqdcDf!~BTjaI}$K)9o zX}LReoM={ERM{ZeRaKIq`jGWP1v~Zc*k>F$)mq=Zi%E7?3%|CDWEz%Gqh(C;wVoy( zIw(2yHIjF-Px4#(q&%ZY>T8;$ta*o&Rd2BMPf^Ju{N7KK`wuTm9)TC>U`MNLuCI?= z`*g?Q15#L4B=OccL45w_{@dlQqX%Vkb)6Kb4C9GPPv!_4L&VA)&PnKt%eSvJRZIGWmaId}p?coRI)tCN}G%x|D{h_s| z-#({o|8@IK^2V#4A*W_L%W zO%~S2>Jb}3=yVv0GZU(ivzPC+07yoWsmeZl=PYIHvr{UR`dibPF)+pu2OivWDIU0I zZc(m`_pU7FoBDwxIiqY5LE@B1frn?Esq`^bn$7@xd~oyc?~(1>cgVvJ2ZBmt`0@Y# zP#!ycVu3gyAjE68cXrA@|MM5*)vtbyeE<93w-C0keC6Ndo$q|7eCnS*E!`b0>*}3L z6_lH>G4F9&FJR~}fN>vY&Xd99C?GSbfNKZW4xLWm4TV?{|KHTqXyGuB23yNBK58Dk zj`mgyV@cI(h|!rSmGw%&Ogppic#SAaZ%?-whRpbqq=ktB57!Q^EgY#yUrv}vAfpqq zMt`P`kOhn$);6-fi4@O-47x0xl?&cq2%x|BN)S(mFj}2qnHtKp{4=UQ19qUn8PCp; zcZ9?p9@Jet_`xJNsIU)JjuOosJ9n5ox3;!hSqZ5N!#q&zb)s#@?|cZ~N`PFFA+oHz z+=#A!4xTG`&_;L!vREa)1}G{lusyqi-sj-`K(J2zXXzkVzpDkC53{96D%Yg%657Z4 zctIN*H%N71trQoyMf-TMCPorZw0O{G*>WH(f2#%e=J3~D%qn<#fL9KW4%?9sh8YC< z2wlW$s^EG|U=7ARQuGO)4}C%iE_Q@lGre}m%q=o+mB-{@mke~bTN-Z;GTD`sxjl`7 zU@cZ*xzlsgyF{k@|bj*ckRoJorxa%vtFD+g=mg0TN+N=cQ{IjI&3vZW= z#TUu^OpT0nRY-MFwKNo+We4y1yuYhx$>)`u_sDpCgG`*?pu}-8xkS#k2Cc_p8EUSR z*$vf_ci}cEzUFe7Y2IvqXK$&Lj`9+_H^ghbfrn}0&>{0aag05Q`mkj;jS2FU; z8|@gr^L{(Wd-nKE@W7))I30i>5xQ4dy*T#q86GB&oy}Wia@_S*yhkti;@{eVhYP&< z`(Kh#)uGkRh4R8Ld}86Z|MI(^%P-&Y5qZVsR~vbH^K+hNUMf6Kd0Dx3A3)e2O{%Qr9AA3D~?Iz`dymFWNTmJ*#A zF(q0`oWj6Eg`3&Wq8jwKlcUp0vSws9kc9@=tV4=x2w1#1rHx)tncNVL2OcGC81Y#U z;qU$X;CVbA1{!hh3^E+28yKU%{>6{v#V>xb+a(svWmww|E9`;=&4WD^#Z z3cM_&YG7l?Kz2?Cs4q{d*c*+M&KSbTE0ZLU2r|2|eg&CH79tr1HW!YU{(PmN%(O)? z(XSMyQ13#Dga!UL|9!f_Mh2I~O7m`yc??VnWI9 zrp=qI{>Ux~Sr7Ib+68%^oEVqjhRv%%f4`gMMy5f$vLoAgLstu)hyF@Pov*(z_$*#I z62TKQ6`PbmTV&}_%Ul!hmyE43VC)i-nJi0c7#RM9tBp>N$gCEqQ02L@xJq)e2;Wbw ze?tyZ^0KS~^nI2>@p?J|8N~}*Q&VMQV;F=1(hpJv>$#XL;yJq2z^3l$BeWYjjV%+9 zy>!T1B9H(Dd&~|sve{U;rhb!jwj5X49+TmLXd`!QWJHQ%B&$#bZ@%TE49?9;<%X^L z5H5nJO^HCh+C4oF^~+11wM(9JuA6LuV7=_pLfKxEEi)rdF5dE~1M(N2KVYu|yX&Q_ zvPrJ{-YI#@JMNSJ^T~bkvA@|b*#kvVGuI&B{A!DR{{D~bx6kML3Zy){-adcFhwqnn zz3TxP8j6is%}w&qT!Fmp``z-kzuPOX{lr7^i|gBLzozT&=#szr#!>nE@3q^#CucIv zv&EV&j?;x8c}f+V%gZb=1o-gJzAdl1^eGF;Bp^I5{Lg=o_x<2M7KqrvmZO%a0#fvi z+i$eBR2&c{NDbGzaGMg_tVUn2f9p+oX_jWlk0F)Wo!8Jm#n7%=5KcqU8;vD4oq`L1T^g!Sl%C)!5i%3f8;& z+56yh2gfdg1fXVqRt@&jhgGoOI9Y1!TY(yz{EK z2i34z%yO3uUQYZq!bcFu8xs@j0oHmkK(kJY43m&52I^c=^3Km%ram^6?NK60VuYTU zcW5vLo( z!75i{DmtaO2Zj-4U>zF!%7Mzt3afL7t-lY(Fi1xDqRQ%#s__n?A&Xo*u0#3U>DyzxMv0g#845UDVHJ^Op7b4`BZ+ivkXOMg!S6v*#NNC??rM<<7+cc|Nl zmL<=~uaYK}>tvpN`YcE_bnXukJX>dHx2>m&B3drQxx(~tGSFX0OoATn?Cdo8V!a`H zg+7D+gDm3(CTx1O6O4ok0`ILNoqtwNmSk%`BI{rj@ZJH-I5;(KYq2JkXij9#8>#B( z>XrJs2CJ6|C5P=(9gv!}?m*|*STPR0QjYwJcoc#5lUm&HM>Lq(qjd{8_PKQ@(rC@zj z=iq>OqVNRaY4Z5+71tVpg7h3|>o8&kVS)%jn4Wdn1-4!aG6umS+uzB)E+b)(q7Pj2 zV!68ReC@kSWtqekc(VZ4JlWY}L<#KgI%#CV=3fM8qEV!{f_`?AZfiv1<>nQPx- z1dq=trw7mB*NstD|NimDxjRLE@oyqO`I5*T4~YEe#@H7rV7YaX0&Kn1+_cQ9kwAFg z}NlfP22a#ZMWSfJI^{#ZvK7zo!)0+ z_xPq!P)S^o$Th&BDHz=c_ z|IW^K3uVJgh*3nj*stky0K=N+h(}E0!5jGDSJw|x6MWYWBU0bkWQHSc0dd7+3UOUc zcwjux?gN9WYt;y4jbjw1rY2%-uyjmXBqtoP4-+6eD?^`w&7V_?vhY}XA9uLV7wZ|M zN``_Y8Svi!%_H(Xx}~6i^`!CFVkH7bcgBpXFNjwExEfb#^dr}-^MydrN22imv-chV zl4VzU|LLxrI_EGmJ#lw7uhOno86^-P*p@h>jcKFPdiOO#q9P?InFyk?^f3 z5nG&}lBo8R=f3!EW9ce~rc^|38ETTj7Aq5`CYNNXVBz}hcVCu&`?Fs*l=sUu6sJ=!6&BVyMFQu#+v3_7jLFTt6qd-jq>0A{5{4(_sbpN8t{p2OjLU-GKLUzT)zH3FMMXRAm{Z4ONa`yyFjC%2=WN;`H~AEAAN z?`EV>K$MsufowJCE4H)w63Z78y5{;D2#8iZosdNHjUzWsK@I{6p96d9AV5*xZWgXw zxnf)mS_)x7yW5-Pi~sPq=3Ee*#j3kCCBtL(nk^+*_?r(545)Ha46VGrvMBf7af|u; zW`ib%g5>=qZP94$ruKTkIw=kmamr(}Fd)_A0#XYgInMe~FDM6QOR=!AZ9r#dmlCYP z=D;B~(ao!3yI_bv^^a@Y`udvYt3Cy>RusfQ*}hRPC~K6?TFC;}F}VS`#Ck_y3f4_g zf0PL>lI5(QSotVR$_wikXCgYrnA9qQmPClvfJQ7?7;p&p&}(hHJL zG(Z9ic+1d{6ON!@AAaEONLje@DF@!+2whN}x%=HaT^eddrFOt`_?@9JJUM7nceBVzj4%*>qGqYSwPd0;)KcM=1l8`I-3Etbz_SfXXu(3G=WL52tt74%zg zU%!cdIWk;INYEv)X0i5Z*vyQe_ubxayO`i2&(dGtO zU0jwEea-U2|ME8Zwr?Di&WxR;3AaWh^}I9BN~;#@mwx$8-)AgDME7&+qu%sezhEpy z|MOeEOYS&w)L5Gct-es$%$oIF5HGA-iLlM*uYAj!jI6H}th?G= zotMzgmLzo?b4vvFK;Mlu9*ZR|g3F7u=3a;G+@`V4Z(hDExm3y!tE*S9ny7;P+6EY> zAXpmmD8V%dfue3HgIRdRqR+Tb`Du9-Eb0?r68?|JMbx9t(HBum6TK zGIT0q&cFN5hvnb@uZQLGRU6D}dv#S_^`b6$_=#EhnO}d>{C@Xs?Q;5P>G<6^zaZ@$ zX}NbOEib&kOJd=?tWFKf+Fb*(J$^wp3hs`zDC9Q_eO;|4m<42qeW$Q4Ieh%_hfIJA z_N2lE5aPpHE5Q0J-1>+R=&VkVBsTu_uGIow>(f^g2%pb3035E)uu8^HoiagB(qH>E z(*8YPFHLWFku<;J1?Klkc`v-lpgi9H zg6HqLCm`%^Iep56O!it~sN=ity4zSlxQ5^OzO%h41O0vS@|V8IDD-#VdAs>7r0n^P zZ~W(GuPi^l_=V5kkPz;6CYUm7)W1Q-t=svRh})DdnsDET}G!edEIgDF$$(vq;0lBy-6faCH5Q4OkJ>cd|~mLQ>g z+V24CDiZ+Sda&0u=yNwsn)nSughB?Pv;jkEkHluv;fPQ1FD7K4bI7i_^`qJ-`<(q|~nn&`q81%JEEh#lFsZC=S&A!rT<{fqL?=~(X% zasGb;Y_OmmVYBRLG%QV87PNQTrYj3G8Kp#ozDl1M9T_z;!4KO{i$J!!GIk`+ovQ_J z{+{nL0fHDmc*gS5f;6@4osgI(g!bx^)~u#m$KFUhBe7JIwDt_>dn1yXnv}Sbsg-M2 zWkGejhHy-7yZshBE-CWb?}tD5DM`%DN<(`};(BkSIVoq3g{3VNR;^dbW?bax$yPa` zw`_JqrK`VL?m6BpE&W~cyxWt~k_pR3WJm5f-6Ci2O-dvjm;3KeOPlJPCyu1#d%t~1 zj<&>PC9*0H+~01_-~YT`dHHKw<#hj+wDfgK|A~;?ad(Tv7dPeJdpqRKuRpHsIw|eV z3F%g4{La2MX;|Mh0fH7TUy?0Zm93_%Y-8o=O3IcJuld%L%-y~rJ8%2EMA;|Q7b5+c zz~`QRT3Cs5`@m6o$_oKGoyXol6>j?l6mpnl6lc{CH4F}C4TaZL{D@|`s7iG9%+?m ze}`mK+p@K@A*<8ZWn*?&CTFH(X>m?gL)-Gmqi-)8!u|4y$hk`*U;i>IbC*=)Klr%) z4yuK4KeWw=u~KDMvc|H-`l@m;Yq$Kaf|~GXGA6kVEkO3gx&er$W3oE4Zua1MrohZO zl~B*DQxKRf#4Q6ijT1vd@^5~)xNz{WKsx=xy&8V)SKlg+pL??K?O<@PMk)1LU;N*} zjoQdJDEqvhKG8#{*zBfE>p*i*FzbsoV{B~9eCNFkzzn|beQbgYSPKH_SY1Uy$4|ff z8YMq!sZQQ^P=&}u)zd+Yu%FHQl0BTHVC8_=(H05hLRmUczfDbPmBaJ9`3W2xwT#Ni!KC)r)|Tcy z!GyeU^-*sb*HW&v)?a%^yDZEHS!1h%Hk@or823Q)y|DQ(Yd76{%jFS*MH;c9(VmG^ zhgEgIlOe!g(SDy!CUoptGJT_7Fp0oK5pBAr&4?9$ z`M!4-{<0Xoc1Z^MkI7EK`=EjKw3@d0sc~sNa*G*Ol{1N>$IN7@!?@~FF&)FF#)l=Jh)6V5d8y7je)|Q|rNi}izP@n61ELH5!vATN zSHIHsulIg9BmeC`F7LVwR@;pD&`6fCVCl~UT?&PDps3MW1v%R$6fDA;UgsNNoQq*e zAdGz?(iD}oDHZh35YfUd3Bfx#HDxw6-4C#6G&MCFB@lOvqk{uCgkJ zU=x0V1^h}X1ciH841Ad2nL(h55+)qN!{Eqzn}eXcyUX-3X5S$vfbi$+0fv%oUH$l6 z545b*3I^mL1>cLqAX&y$D3&-70fi*Awsn|2y7m)7z8;WX*QfY|Y@w7R`<&qPTGPrA zCD&#huv|kX*vpIS`5gomuWQ=?WrcnY!6fSA+S*FsJwZTbclUJ5oDv9J^~#}ECEdYY z>s6|2Ap>4u9mJIi3*qq-$F*+fwQS2z$Q=fHdV92=hIJ5}(7LqmVxKH5hklYpnK-z7 zI{3>8r4~0`!fco8lisqb6}6Tr28?QJ$CyBfs0?AhtNp-4_l-2}uE&ZcqOv}}Uh>{@ zdF<`&Rgym^SkSnJH9l?~B>n54GV|FAtl%+%+$aIx$Zp8u0&Zv>64K63WJWd{ru`B8 z26xj&?f3KnA_*erkoEgPEwbI=1Ap{3B^ukZRgh;`ve;DHi-^kaaI#r4+D`yWkvDvD z_}H;y(v-m>=5zlIAXwF~`ezTx!uwt(O;>Z$KlCb@z3|sEb<*m7<-=l3SY-nN*_H-7K-TPFeZnC*|T@#d_)ZzAslLS^p0{?1wa||+?EmOX4R(; z+c`UP7LK#qe1-SdfP8jak{xl`(hB66HybEvR0Ol2oBME=VPI$N5lRY5AkPb{pnloQ zUs1T?nZ^6w|Gon&#OOIz;dFzxw~U-3R9bG#l0ys*LO_CGUgd*L+m1w(6k!RW4roGEwG9Eq~{J7Sa~*5J|3$S2B7 za9ub>hce`7kmjb0A#Z^0?(g!nC+da!oN#t+YGY%~q|>Z7L>b${)v<768IUJr0+Ib} z`?Yf150vzJlz*P%KHYQc1p^S|1{XV&%S0lkc|F`d>j&?G>_S|fYae}#2`}5WY(BL6 z`SQi5_b3HX1tH|L4Y%&i5)ONa+Qe$QlhU?7EH7;xkQ=Ns~d2AKsOJ>KmEqnNj#C1 zh?1yiLS=Pxr$o}NB}cNkT~e8pMB^zXjA=4a-uy%HCFysr6Bbec`-B?tZ>(UcDgM7E9!^`p(#;oP=7Q*|q-98bF{% z)+E2MVUAEj>n$8L1S)aoX=#4_De(8Vxg1quYuavjF{deUJ-~4T|G&`y7v@!{Q^vl01sf5T~0rA{e-h6zp zc<|keo8~U)z@As^gVn&yN);l*wGf^L&A(YzZ7dFFKaeNtgtaRuKweOXZqF@OcHGA#2kWXJL|BZ7_!WfS-aez~ zqKKC(dw+TR`vNjJnN&qbl{6xWp}Z2VJO~6p!EM%+Gs?CjQ%>>cyZal|O19jy_}nkw zr*5~kjo`v}^5jYVoZ9!ZY6bO+t2T-)Va&-xUth0@j8|*kgMh^lvN$ne-F$(!Y^ldu zA{9E+t5^d^puKeqP=IN>`?VMm@FJ3F++BZ#6>Dv7J+Qp|?}kVt$AZ>@xogAWm?X75 zvswR^_9Fsw+-R%jVSqA)usf?9bw=by18ob)zwI1rWbLNHOpv+0N?j4HlzSO7D&?2^ zg4SnMLeO{>gb#i2L*|pd7>&hETVcE^ardhiDi0U`?kkdg`eNZkb)=quN{ee5WD&Cf-G!~$x^mxg@s)8UtW{YY4*WN=yi><{>0;Q{&;Z2e-@@0W z)0HhysTW&2GWrmv*Q33aBnY4rtE1C*teJ2En^gB}IUF$@h5$c`%n7|yKzexxd2{7Npc%*=Yj-&3%8kFv>C}7MomJealV(+5$y3l87s?v9Eb>tc zc%~x-PC<4`u!H;E!Cki>Lf}BO$i;Vel)FhnpG{oGpfWX=co zUaa&?1`w6A8n~yiF(O3SMDB3E3r_QFftzIElYHUwhwC3$UC&Bh+Y)_#OXXUv>tmU) zAs7bxX;yL%L`GqKt(ANtH)_F!3$+uWYQS=}^0$x4#%T2r?S3@9;sx@tPfp9nuP)1f z|GuNLeIz3j7uMxB-hN5G`%TAWaHvsMw-zMQ5HV}5(xcigTkU$SY$PPfP(&g_$7SQm zb7uV(>#RT~Z^zTd1uc`ES=ni5mt;64F}>c4d~?PSI*07FtMb^Xy*k7l7xdbd+%Cz= z&2CEXPd@h5?=o0{Kt)a}iApA_y6}NUxOvE~4Qtdsvc{;VB_{vwmNU@cu= zmaSa%eB5Xt5Lp zJs>UKgS?VPK7-Ooc@U|RNdV`bTGi%C~bCd<- zrxLkxAf2Ucyr?`o;cW<2LKO#9{+L{#?f9)&^@4XeP_8bWEEmv>5hOiPC0*la!w&VRqMBFcEmqZyw7PEOX z#A?@FQsoBgsaV>uy_Gex5c$5}dSilt{CYi8%7Ok}%UZ1}O>7r6lPo5qG<|u-vjwgH zm08&yRhgaB?{%!VDhEi`FMniIKKtmB{Qm#EO~M^ww)jczhwN1bABp`cg)=%l5jA zO>U^1nUg*xX^FVj+vJEWFU&};aYa^JGSa`)WGq=%Q&Y0gR`dtkwPa0RmGh^HbIIgp z=B{3uoLiQk|M|DbvEwI=Ma%q_+mX?cD>8TWsj>u*`?K2L2f{xj|N5sc%Two*(mz<= zq~Ge!%_dnfRdAMb63@h{y7rlUwGgm3VSSV#SR<>Nrh^f+!vtl^qS)0H6&z*Ue}aJU zXS2;qS1y(iB&Xy%FnjA1OcX&Beq6Tr1YUCEb$2>`;`9xR=;lYcfkE{;Olr484aSR`&{;8EZKYe`U^IT!@lBZf{^E{)~D^^g+k$oS0?Bbf<>Z6!2X zPtGlkyqRkPKpIel-9BveSL!*Lgm9y@grrfG8J4oFZHgyu`tM_maK+f;zNrmU&a>kl}h{;C{e6J3HHDL6r)Ea9}}m?<7wy zZ}k9DkMhT^gu7V)9TFj8u z4eW2;e9#XF27>#fmylONPemrYf7{pd-Zmg_edh)F#AmKcJe`o9bY6byM{bu5$;iL? zPoI{5^%HloPA{OVZ~c@QfZ zn2m>|<AX*J4}Vd{`}4AJqTLWI=bkuvYv?H_;wSqUjilC ze%*Mi1Zq{szwczJF8o=qT^~-|6{CUT{!`;PTuoEEx*f> zU7u6Adw4FEtZAH|8j};ph9uT=+<1BX?(e)!KK}8)GQa=QFTYi`SPQzoA!|yK{Vsze zm(NRcr#%;LxFkKHUy;B6;sqJ2)1l~(Kk-bEthKB};*q_f+I`^K1{G{uYd6+C;Mqv9 z99S@m3lbHC!ilm`4JhTq!^1}K1C%-Zzj?-bR)vBpXeg5?&Q4jgGts5>!Mjn=uma#l zYm-@kt(!cJj9OxVFvzju;gbK2Q zdI!AAe%gEdN`0;H-p}7bL~lRPYL({ZR=IxNZoZ0gPLxnwpZ7}+YJqbbq#t2@@)A4J zRBt)aCUIA;mkhZ20sB?bgMi#frZciIxBqQIBB`RHWddF->(~)65lSAs?Zd7&ip%u0 z)lzCH%SvquB23^J?NE~2+^pHHlzPMsktlyylPzpousm;Ws+yi@HtQQ5SVxYVB1+Ph z%6Ed!1f?H>1^gs(zjcVbZ)t5-LdQOS)h8K{@BYav2Tzl#hzEb;oo4Nn+oZKIxp#*D zX246b{D5RNwYABQ{O8ZffBlXD8M=K?-uz!5mIv*@Z3tSI9@}z##f(`{zszH{=%;*gmK?6A>1J=OHV&s z62=|e5Rw(-z6TD0KsoY*)g}+ARs!V5ou}?9{AIDF&&h7B?jcXWe?MdjG^#xGuA`bB zy(Y)*yx&YB1QDHj;39IbZ`gjav9xFiRw~kD+(_T?-Ve&WzUPO3Z1I_PyVF5 z^;dsgCdY?m>v57*sjqGiixZ$oi;@c!oi4k#<4H6F$+I0_T)1rPwrhW)oFGZcS15LN<| zK+4)#K^U|_y|b;36!A{}b^|BR?tUaP7{Ov29y?v2vdKnKyuUXfWWr6lbAe={Omn_F`HXtSIgOv=YTJtA)yips#P zee&r~Uy||pynNH^2BojHQ9k;GCHe4&r(|t4BF}$L`<`qz@LxHcD`Be zIGd8i&2ed33QI)mVe9&N+0iDDU!Icjr3*51qO^iplhypW1ql!B-N3mTa#t5hqVUCg zj!HvMucXc#ljOapCGpC8ccbt{hlYw#_*zvaZmIsUGArxLs_QA4U`*MLs=lptxUKzR zTif<_szu^@>}daa;>tOtMR{4us{Rnt_M#0n5hr4#_7f!#siv0QV{Lg+=2w?wY-&U% z=O)cDJvU|69lHC&s;9(KtYh1Ie_UlC2Wx^u(U^)u+pc(Sy1iH5y)Bzd^X8g*01;ZA zpE1wLYrl+Uw@mc%v=YyO&H+h8Y_OKbM60~_&p#kP^6&ngkpoQj{NW$`o)Vxp${2eY z?&PFlD`(Q?Kvdz`i4oPUR%LGFvXZuWiD>zxJGv#)*)NG^>wcP7IkcqZF)(yWmh~FQ zRY>JZBpj7!vPs_iz7MD_WAz;`xpFvkl4Uxr4qgFz^Gyc?Yju20a_jq>;XVvdcC{1+ zf*qxibu$ng!ogz2*fI*FRaV*0hwHpn8`gEP#tk#e0ZcSc_%tAShQpESK_poe?_76^ZDViQ zTA0%{z>us)C2)s8)PW97>oo(-$B^$U9u>XlD+#z0}xfc7eACif`Ps*Te3U^}v=zF{J!{5Q>FOS91B-X;e9* zx;(CX<-mK9p-FA)>q-(RTa+BOVc=Yo5+5wjj6w7@&N0SinljR2!-iy7VAXR=@ zQVH~ly%P;B@@{Q2jq=xjKW(g9Z~9lq!YclF4b6A5|6Yva4;UU6T)-2cLqA$7MO)i#vgl$G3! z^l$fwDA9QO$ehf!uF7nCZZ~YZ7txPimQ+4da|rj$efLV{H7}LOEhm&fo!KQ(k)eKR zP|gujp5O>nc4S;8uV0X*?FL!i*^uRitc-3h%a+!6w7t&|C`gVw!nA#re*kkpjK8GP z9f0$*i!;*J)4xl8IOr2W;mldzTPt}Ay1yJCN6EHsX;fWqX?n~&vmRvER^`~>pbT~% zm5%0iV;pEnHA_0KjaStqHxeXtquw{9$uYP<+WfEf99~mI^LRrixIQB%#b2-7f>R5u;ubS5ZH^#OHVn#G@`}VNThm~NBEbL-sdbC@ zDO$?c9i%y^^8xCo)}8uqG$@Z;&IaD76;zWIMmB%6L&_7S!C9C1J*Ykbb-|iC%9_ao z6d}r_ zVwRp6b(AVB0F(piEGMWdfqQ9#we$fi)2*^-iB+*I_@v1Av};bx*}BtH50F!o6X`P1 z6PvLjr%aFmwlu(Xlt^_%Q>2Y>EoDc#NIzu>@j^yo-Jm~Gj*!_(%5zdl0hPh~9bA6v z1;HB9Ea=wyyjj*P>WRVMn@1*$oO|$o;oTbxK(HEhJkJ+g|8d<#4nvCh8*4Mfi1MZl z(%z9Hv>EOo&o@l%jF0%p`V)@vc;!+E14f&lJ)i9&?2FR zh}NYgSueV`P42q;lw7+sA%FDeV{-mVgM94cS0$gw$X&;q4XG-J z9PMAni;lq~^19c3y|m~U^1W~VPKh_C%u*8MMo4vK-Vd4d<`e7I^vt9zFRIR`W3<_y zbY?Hp;tKPg3Nv3f8qrKd)@M{mua>tC zJQHwAUoBvuW8h$Y0)rWf)b;DvY(=76XeV{gV&5$q6V?L;AqFf4M(Qq=OdEw2r58b5 z4m{TkoDpVZ*HB^1Ik!&cM!`EU&~Er{+bN1*>F;Jx^pzh@fru-dAEiL<^mBQSr?XvLIz*;ea(_4IwJ1n@z{YH8W z+19~~{&pi^^J-_=AqcnxSrQ4kuxnnqhIWcYit^yQQB=K|Ed$a|JFcbu+4?dG;#(nY zyo*2V7yEAeTMe&2Fvufi!$Fx?tNNDN!>2_Fc(e9d_L4&Z#f8vVJGAc@%e&^8NT`O4 z&CjXAv}&H?)(7d+^_sm(6P^M9xdH+PR;1_H#eOT%T7>d7Jic2$9`voHDbA-Clu- z7H6?$9HdQ9#*Br0hm7#thEPRgOR$2j^}o}}t9n2lxG!V8QEIcYQHS)0THD^uBw6JM7Jy`RbT-N>Ivb=lp?$SA zE=e7ipL@DV`i`p{+-Q`Syy%47GSsO3D=x2iL5tjdYeEuDP4e8^8>B^v$jiT`MPBjh zj68R!Rl56{q<2u|c{nb&ob8u4|MO01YEADs7mhW^_2sV^>(%aK#>6-k^rt-4xHb~<{bp{qwCD#yxMpd54j?|qVM=-Hoc>%G?>#?ZhX;bCRtjhnT z=~3w#I4SRb_aDpue9JG&$jGSt-rIg#-uy5Bl}t`4aoW+bcV*e6X?A*CmQ+q6SJ;-J zsjWjgd-|oRrOm9l3gn&opbVWdsKrz>um-U1#}O>Rz=v|iZt@*Uu9yKv zNp%DPSVhvV?W#pk0|qfR?c`ZB7M_O{jDfV)XE-pBGfVHik1}9D@G8Yl=?0VwZ4_%0 zZO^Yf+Zp)6mjY1gd5@oDF}sZm53_=9U5~!ObSji-7Gg43Q$E|qbMcY@m%f zqlCHh7VfzaGS^0mgv-GyHl^o)IUR7`g7faT9aY8?roA4fCCdv}UQVX+PQKH(X@BIM zwr1R+v^{IfH|^IEF0O}AltXJ5WM^srxb}%zZ;F3%_W9rZ<@oT1kQ?^V{nFx!M@VgtI7`t~EzqOc{ zxmAi~#sT(~r0jU_v??sDiR3$N4nl;RIeEuj5P60L39C5m%N>QM0$gsLzky-X=aUZXYDb8w~Y4&z`(=?G z2fX*X>r0`;?*?yN~@aYnYa53T2?OU!o$KsOi+LDd!Wod40kfX;N zq<>J!xjtiIb5gHKl$@Jbd%BQnypMrlf3T(ACf=)v-ikrUh^8+Ufq_7=?Q6XYnE^_W72@@V{2!Rv~+Zt z^z7(;JF2U=vSN~GYI0P0gvvR6_KJQQT7_PUPOIY*Iu1cZSf_SFNS5)1*32TH8ug_J zY5br;Bo&kGXDEtaG#!(*IVD)j&n|*xWo)tVi%Mg4KysMvW6*M7&^ErQs-!Y2h*A+$ zx%&YFEi=iy58}Zf3Yl;-zO{h--t)W%RDp=g{A_Vn6~gn}`(Gk6I!L1saGf21O4EMo zmfK|D$dHVW4j0PKVQ;yiJdkDvRul+kuXzujd`Hnoi6tEGGV6PkSfK=X%ZvIb{Z2U) zz65~w&LV6Srun&9GqcV;Ji8v)bs@NgRCw7<0(~5Jqv{K<`L{S>;0L4^lFWb~OtLHx zQn-bo2&1sMK4I4a*)~z^C!FV;J?{nWC#d|Wk6QZ~Z3yMH-ukNtHl382OHVOfq?59u zEH>7$MB6rIi9ue+Yb>TolTPyGC0rw*X##KLD}B-b;c!olw-;%ov-TaU&d8mtNQws)8B z>IdxwvPXL3ac50Y5~IW>sRWL5ga%J0_+9Hy`#R*2-?4skpOshEO92ALId^cKISeV% z0}`g^U3>59={I4v znj4$QOW)uz>FYnD?}*9D@{%c+{v*fqw}cEFJ#MUQl-tnBGv*!SyQhCpRtlso7_Qv+ zg0@P(LB_L3Bz4TeWLnUq17wPY^YkkNElWqX!ZEvsi)*}ust9+^_7yLR{bn!hGlG9B z2iiF8BGc4lh*l+8>R=`eNL&>>TS!)3^_tx1oNSJa8_Q4Q+b_!O*QQD=Ov@?lzf%`w z=gNe{(`^zvKBW5PrbMsK$<~-E!LwInWn*0C!*k|7@4+*&+bes{`H`Gc9Va~`TeH_> zqNDOA+8uwqWTXD+I9Ca*!`fb&lLqZSU}AboMrKF%1eRFaSe3EKG3o2hO^leewY9n>a$R<9MUM3h z$%@KSh}E_dHi!A~D~9~A?#$)4wR=!L{_&5?>t6RdGcNqjul~He_6`3;<&=)|(S&jP z<9crzxDU$~RxH48Sh;$So+v3xvn~yCw={3oaJhAAAN_vTW4v-yXGa4Ib83t7o7Kf7E6fmQa3K-uCPgMB3sZ3snT&g1^UV97?B z1jry-S*0BadMZw64{ zz1BQG+}K@K5AwiT8p;ns#&xr~Rm*O}1UjHBor1;QXh8^4V7RYR-PKdC-nMijP%WMA zpbl`^BvRmUBC^lEQ!RZ`Px?t}1R!Qq`)CYLk~>+7r^%1OAe_Kn{N&&@$3JJ#o6 zX=8F>ZWcGkc@vCbY0-Zy6%PGdNi=`2uC5!R37KLYXD$c2+~~E7=JVt&w;R&NbyhjB6O#i@xyUzwRM&q)xJT0McZ9zi(h&MHBpAKHL9&g#ZE^c*sPG0q@ua$54 z=6@>R^&Q_Xul}a*kkhBmNN@iUS=Vuu=dl5C5M0o=N}!xKRkqiI`ZWs`DBBx333-$u zqnwq@_}pATur?RBWn=M~A$XO*;1i@^Ii-r}K`;7vN|8N`R zebnvD%(OAtLhud~xc?kKb&K3`+nq)k3VJ@}aAfGDoH=`^4%TOk!owm;tX&MEC=Wv? zZxup*qkN#CCezJ&{#L!_4(aSFDhJFU51lw;-bbf%<&M(t&XbNkIE6oszjxuqfL5x zdkkrS#P;|1nZGG(JJ~fS}T zpNWvE2j?Mt$P9=f>9c`e?BAt_Tc1??8}jBRBoaD4KwvDnE8TkyPl5AKJ}y@;T`+5| zutc4I@=^2Ni|3zI#l5pc&h40S%9bDHI6XBg*RNfYh1qFCutu+6(e&G~=-TBAhOl0{ ze9^ddcK01I&&O@?>ZS8~ZdR^exu|8>W3Gn`y0k#Lm|%JO+#_ZpjO(kRp8mhSwruK% zy4u!q>FMb+la*fVC^5zg7z97rVtoe9%2~AheT1^IlM=ztJLrT3#2iwNcSbd$MNp5g zW}=mN!6|8b<13`?GkHlbwMl!+5jm2%*Bq%xr;I+nDB}xRX>5ia7uI2*t|vvC;$~1}A($6iDnyAW_)H+2&`Mm5^Cv31c9YIPlv!hsO zaCxB~99*aMTIs3*LWUd2vjwy>uS=>QuAS1RE5Rrg`Jy7|1#Jlh3j#JjUw-pubOd)e z@I3cSOZaR$%IOpJkQ&-esEpEeSg0lk%E8@7AEe(yxUg&xZ4Y04u#wFf($2&PuD!U6 z(TDwTiXtfeyO{S%3gcmewJ=1Xb=F)05kyr9-%;iGFhD-Mgll_i)5y2uCyuMaYF%lt zdXJ2Z$i<5n4S5u9~mmCN3gYJNja{DxcJ}u{1?mfU-)vl z`#JZSdmZT0p(rHVrB6SVfUzte8B1`DQI-DdBKa!dyw1S zceqIxN9?u=n;U!Ep&#zITHB-dS&&vN!9@1Nx`w;0`yFf6_U5)U`o<(LcF9(x!H_K? z64*7td(&GfeU*Om*H<|>QqK4TnJ)*{lmkFPPs#Yf1T;cZ|qqzcM3hAth9?9*GZhO8%Z9nLQhm;dUMGwSQmJKKzv@MkL?d zB^`G?U@Ton+rP$;u9Nepq;FbDw~{XIZ;ua}Bhr1$5Vd^AUYS{f$JdOtts3I}rzD>n zmu=6a7X-Opg7MW3EJMy}w7q5X&N`-g;4F0c&0t*#tDn~bMLBagOV;H0qzoM!lFxtg zbJE-07D&uMYgeDy+}I1A>+-O?ykPe1Vsm9CY53cHuZG$+5yj@ph87lkP?Qc94f-|I zXayERDbZ5hw0#&LOnY`ME9-O5zQ&9EtY=WQilhBIql)O<+`JAH#eCV2<4LoK`$m9) zmIdSxJR7ZR!6*`JXbez<7UpO5Tx}6y$|dBKZbuE#6Pap%3y+Z|Y#S$OJ+M`ZN+ zH8cCjOleE2wVpXZnA$oz<;umUwJg@<@`b0(cfdgZraOXN4QYBQNEerfCTjR^%gqI;AI;4zdSs-hr$_qx`^|Cc z)CoC${J5MteM&!%nfnsSjESIUpN|_E+hEwWvREE)FGOZLf=noS;{sM#7kW^vX#KM} zCUO+ZJbNIP1D36azx?^#<4d3aj3J$XzPVEf(GGS(`d!mJaP*ieEJ+!;c4e0c`@woK z18sMdt$Y4sk31-kKJ+E~n+FYMO9uN+5JW-_(jL9f<8KHy=t%!t?X0!vqOZC`^1gZSnEqEU!T4$imLVpknp@h8zPA%?HbEomMJ1QPM2ptl zV(~ZJCugQc<(=<-pE-W;-~EvM#~=HTGCVUPE$uzK1|q*}AY{&~*J0U$6i<#_GxZir zv`JFQIAp9D?-6}(>);6)IQzW99pzDD7cW5Dv)@$^AQM@ag{3Q!h{?wC%^^Jq>io0E z<7yW*$lg70iY6{?>@Bct76Mx#i9(JKJ2Rs^8-@Qcfwc)YM8G;PfU*Xm2o+{80YctS zFe+hm_^N#Q^MAK{Jn`@scLyB^4+Q7Vd!H}2+tZ^N7vVpe6Bm>W4Ld6EjDauja z%YDwufFGPJy*s1OS3?_4W@IepRKlFrGBGr~*j= zsUFhO+Gzq?TzdL(y;sM4O+V!T`UY%Zi=>W8J-vfw4OW)*R+?A#cHCHBQ<9dG?%us? zCLp(z6>G^TBQ}Be!o9ha2P}@zm_ux`(0%}e_b{{ zd0tjO_N4ingY$81OYS<3!@$dq<>fW6=#*nUs!ysOva@ED-dCTu# zH^=w?)FVoy*5nf(Up1uciO0s|XaCO?85vtOpFjC~!}4Q4e@=enKYvclz3k%9G~lFMMGoq#;0mML5Y*On%Ct!IDp@xPWQpL)s=KKcZt&+8iKfUI_wH!Nga zpH4MNbJw7G50jUvj(+JFIBmWYNTU|i7hN!67ky$b^I}J@VSy<#dE4vS84lL3YsU75 zGR&5U5tLiJ_gMlvi(h{sxJ^kXC&zac4F+n2Duf3mnEIfyv2q>8Og;<0aWf)71heB_ zKyVn0STM*88Q{IlZdQ6X?FK8>`r4{YPK+0nzAY0*oxvUQMbxTeCr-=LPdq9!lcPpa z;T*p)Bg>-f`PnIHY44Orzw!k$kaLWWUN`TCl#N`wYz8QOqlsWOruQ;{I^NB~lynCL5nHNA=b1W|}8bz!eP|&HnTEc+0 zw6gi`hEcYjEg&)*k=Dx3ycYi0g}u&^6{{rJ(9x-6!qg{ubOeib%-TMzFne)h1qONY zAKrJ;&LJZxyNA)I$^p6HFWoAZ`eWw`5u zU=?EYWcT}yajT?VvgQS{VkS!pvTA!fD=W(ccVH`mEu*wDzS**JhXs3XLAbDpmIJZ@ zg@Zbwjk$W|llLMEaB)L^x!>$KvKN#^H&$Uy``E};y`Hs5tA_OS_7BK{62VR-NF&!T zo8QO}`&Qv1&H7fn5r`l-tNk&rdFwxNT#g()rLvGfAW3uIQ6d&<`{FtMgF_}b1y)AZ zzT#GV^!Q2XA2@1A=jiYi?K^9#ylxmdeDwG!6RB%)Zc58-(?mENR$1-ZCuKFHvY%&$ zwBJDtd8f6O*|IHWy;#nbK;2d&Wb_l>%_n0mlN4B~LNV=Uyr)p^6@hrrm*MJaLYC=$ zt__z00u}r`+m^j)rG2{zfWoN&ICoXr5p9mP;^Y;CUEgTNIb<&Fn)>3Q=1u0E7B0FV zglPNvqU7h-caP17hb4P{O7hp|&1Y^w71POO*|@u3Qvc*$Y5MM0N$Q(lD2dlS#~ktJ zAC*{pvoxx1IGfb+U73^o7bhimZd{8;%P5Wour}p7+TSko`H=k0gG<85$VSDFU0Rb@ zylhBrA8M8NeDtb3XCNsj@937ha|`l~XJT^4*|dC0iB(T`TE6)e{qng7$Kfi&xVM9Hw#1-oKhXD0}E0lE?MpSt8?Qf zT3>vvb10n-aSQRoXPrQmY z(b?Hyl#g=Y8b`8NOXa`Y-Ze3HUD}jbL9!;shE`b2^2UbUVD(0! zSWXU<9ol4Y{<*$V+2EE&w7Ou+k9Ch@-y%yQ!?MU)F74-0{hLs_o7pvee&u&m1Ii|D zez;%N3ieV&i^e*@t9MW-Oc%%DKJeXyJYjLh%;&hbp4_F7~&AWI6C` zXN7S0@JV{mS^bJU8`OS*_!Xh|32iZ|bEgTW+} zb2;WYGt-l1+Y9cqq{jSYKzQ$|i7{jKnx3361PiPvpLv3Wcd*&>$n`7cJ+spj<{D(QYk$ZO>!v*^6PgYtoJjxG%jZoVT>4{55++8koBQWy zCRKJH_mC{g+dhx7=5NX!*xO6XAGqs-esd_cAFOGR=5mxBq6%vW(mFIGO{ybix;kX%`CStI`XO0PhGqJSD!^YF zHyUiEjfsJmWU<7*DHWEl9m>e-zOGN!=C|YnpB|ChA2=dMIwSJa@4g~`{Me>k7+IBt zm2EjX)FJoY+9`kasVnBb`|jw}Mjw{#Y)=0B|GXgS=#Kp04-T03;r7PP$L0oj_Jlm?Z}UhL!>WoouI zg7J?9gg0m6%n<%~O6kG2tk0Kjvs=<4`=7n%PvzhKyC0SJzwiC>+OK_`tg1e^I61s09o#>! z@{zr;Sf9mYf9LVrD-tYls3a?r)BVu z>l*OuMXV(($qr^D8zkqmo*~1mkgOu4eV}|>TiXoTVfLST;`!a(oo4-;KOg}Eu=V#( z7=>g%fLt-ymcqdSwcA-Wgb2YUXFBHSKF=rkmM*{Q9eb4hB)dLQvV_#m1tMir3t1 zP=?HoR!Wm;)@5N8C=M)Qv>Dp3bCbe?#kpYA*H>-?Y@3E;mD@o1us~jmhqD5@GC&W~ zfAw%>;y(9+^s;A1wSCP1^1`g;)c3hab#1Nezh~REt=ozfTo%Y(6ihS8Rs*y-NGKLD zfd||@AzTLwgA;DimhAh}!}sDj>WuPoX}9gq{cRO$ci3{}73P9nB$VjT7qP;)!I9_K zXSnMjq4qxi{lG;ZWW80;09y{cryAh`e5da)xrV~b-}EW^7WL@*tj+7*^m)Ova24bk z4&h`-vKyNw5+Ck!%$(Y?EIoktJI~-*e$an7&nJ0ejB@Ye{iOZGsauRCPEW{*(`S|7 zjasXkAuc@MT&~~`TmGDL=k2Hc;s{ny-IT&Uqj;UogCSd?sH9caL;CZYCPTI~Emfh{ zn(MTIMdD$7Bn?teSGd)dW%;xQ5j4sLE0!H!tdi-<6jx6?HkjOk)FR6Z1!UfWHOtjg z$fU19-uRn8DLXGZDY<(FW%Fd0G#qIcX-i3?)|DRwG;v2FnNr2H*-X%^uIt#Yq zhGac(A}*1ZjJ)HcGxEs!CHZgPcUs!ka}sGxNaU$;X?*a!MCTVIaptTneBd*Z_{gLo zRn4E8mDGgZ+Z&eX>Hg}Q4*L+j?^Y4kY3g|DMOf?Y`uKIT?-L{fWj?ND3L*H%3@k#i zymADU`Rx11#=d@XL;HqQ-FEyo>2B{a2OBT5)dc-A8c&$=V>99)lGPYX%EFkoL(g;9 zSC=H7YLlP)nV*nXzxr$C-se0=jtmY;duOkXg{`XNjhXS2$p~Bo*{6@avWPm`l*&lU zzzIpT)E4Ej8p&$t>FEm`8JsX>jSPc9cXz*pGkF~xwY@Mi+73nLK5 zjR@x+%|s;87L%0M0i0u zX?qORlo9^^xy_9diyLK*Qs$kB2As!r2H+k-({%O2S^^tZ)XqE?&+9;hW~p9|$xOa+*WRcs2LR(qBuVr@C1nH~(RXQAFr*6wtwH;GcSpC3jg4rVs8U!S zI7Pc0tdef8aewnknYgvCv{5_e6|buWSQFWFePVo~aOQBZRZyalnT8yxtm2dJeSN*g zJ%%vyelS+kpZVmd1iU{&Deco+T7Np0Z+n7Y90n-MuI?UHkO|Z>Z_1QBU|Gdwn)Ffb ziwkomUx4-i%9X#ru4}0$SKpO@D=lq-$qTITSyc{c546i#LHlKYEy|LKUYB1lcxENI zw7UDWt7`k4H{%nNeqKPl&=J^ngf(Ylqa#LX^aINz<0}&iM8W*a5By2tmxCj#@`w7n z>xC~d-y7DJWc4W<5iq4nZfsyc8kLykRf)}xjL15Hu0D7{cGfl|{y?897eyDm)bmft zjUitE*MM`>jvOcYW(Ea{uwD{Oh+oYSwFkiIqnA z&f8P+4c~K@{QR$f$*jwYrxWrMZ#p7B_rISu*YF+ukMA9nBRYm{Oery3wYt^TWNBcH zgCO&Yd(5XdvY-Q-4TlSB+^4(gB^jnuEe;^A2WpM4()pMo~4W7A2qCPjnH-6i9s&34Z7u?J((en2D zIxSfYlB$fxbx<^8fIg_;D)4Lp#4oG^M<^VsY2dFFSnM*rDakF_hJj+Rv6R(DWEFL* zAk|+MEUbn|%&0Qz+%8a1nIT1RRsw{DnL-+ga|@^h24Bb#19m;ci~$f!2kFb^vPMZ_ z{Q*jfpB#CCwJoCpmiEJXgCO@tNDc0hwhW60*P&qAbr9NU^^}`?MtYr6@oWI?0zzAXvYfIPcT+wMx+ZoS|+mE{EItyR!`Gi}_KyB}Sh z9fr_5*Wzlx-Is|HEMN2)h!(Ddd$IRjl|Y~hCY);V9x&@z6o!;#ZMu!?Ao?0R-nmOInM;DjKv+quj?6vb?lx%9S+m8(t1(-!PAt z2EVzoFAshz9;C@;tMWazZp&Xo+hB|$&1P+MB&Kaz3kNH!O;q8^3eal^-eUIH!m`CZ zdbnF{*;Ps>DE`j%3kyr8uQCbd1$9du(>AE@&dyF59v(K=`sD><4F}gTQ~#a+{96mZ z92~gfM`CelS27JDedNnulC_pjSxz)dxVKkiZCRF|dQ>yABw=m0@w2x`%j;es$@@>r z#WWIMlCS&D zB;|I!4@=ey?`e{b0s3QD?(dJ9>t1qeQjVYOk-N^OOBP) zw|}f)n~^o`=V5IRnRHCa^|s3Qc@r^^;5um~nJpb%W(_A6zIgYbu~uFvG83R(tg%q zF+0VE!;=#eDg^EQoa^oDHCC;l3iAWv0+~TktfV9}aCCIEmjt*#7W?FJ30Uw=-2?K6 zE7-Dj7;PCwF)<2;zSlp{1J;b8z%(}6^*H7BgyQ}79X78{3H7GU4cNSgK?NlT1&6Xi zIj$6J!u$N7yn26wY!C#5c2A&@dJ$9NjUq(kvT`7@&03*D9tK?gTz@T>&q_dnq}&;_ z%YkR|eA_1NVc&;*u?Ipnl!pH zwRr_Ly$4TgE%$Rx@j8-W%gy|wWt=n<0d`#A@>#qw4 z7UT(6+)ALEh4j6EO(D(NSxvT?uq#YBgd3DlD5)V@Vy!4D&$bNQck)p!D@+DZmL|fe z)<10*uL3L@D7A(}>F)_GH-2Z`CHw5T2#B;5w;vf~Ovh45rBg;(_xEqx2l%uHX{;AK zgF3{!fiyZ-L6qlmpd9#}vB8lpBILzeST9kyNZVR05jF1bl}U>|=O;njLtBFMktXub zfjr@ROLNAIQ?ysg1vzWW!_}S58{aDvtWqFDEE(p0d${L0xcpRmu6?&9BzDq{z$j@6 zX`jae!A!epPPOz8y~ae!)V2~8+Sw_RR?hns7Qj8s=t3|_uRDSWA9?lW(+l?-ZM?m`!|a#j)(6xJ2o^4!xSgTQ z`avhhl2wltu^K#M=`NTU(VXs`=zH}LErM3iXHh84IxFKQwfBBp)40$^g2j@ovO>w3OFM|F!kCWZzBgsQSIZBu z%5m{J_r2CU;NlCIk!KqQW+#y$D7VPc9hHeZFK;&3HkX!w{R;tdj`cARFtDWMi(CXj z1j7Hlmh@T+Bx$j-1hPfhFKd6RG(l1cMo~{FfoxIYy|`tb?N!jdAdS3_bd!E229fdp zcROne*HezYy*-AEUb|L|;O5-l+`D)l@Rms>Sb|l&-rZjgm7eSBBB%~XBggvss%+@E zXRNkNhz2!=GQhG0L*qUs6sqNsysfU{8ozGjs28+N$SmU({n?9=;p^s;vSk7RtCbh4 zD+H9u%in$cBZV^u$Gnb}eMg66X(KPkhK5WVb=b;n$nfx}83Q(!?WAQ~^{D2aqo&<8 ztZz$p{IWC>(M<_QW24Q_`jz2Bb>X{_NS#*2`-p8f(>rHm@#+=HwX~W2njF})wzauk zT9gRHwBJrH&dTD(vew_tAzA(nbFs(+>Ht?y2%+2b_eOyCH+K)o&@H#h*!d?VyKXoA z^_CIfx}G!lm`Q?iaPE6q?Hi3dS+kcF{lvK+b}Er`&xK4Lz4dOndgY3InF)4>59jrM)q+H=e+W}8j$H5D)(3B*Npt< zUY%I7$BOUZFfarZAKm+rX_26epb4V9M{}5 zsBX|*IBPLJF{$;tB^{lea^#p2DqlvR1w5lii^6UTjU zabdAyICs`E9ac~bEJSynx~n9p1V>A%S>LlJd2MqyG608h15CF{Ye%On&(26#t8*>C zxyORWb&*h`mX}W5rmU!c4+5ai{X6XYhDQKIkvK zM~37VfAJUPMK63n{_}tRFY?k?yj*_ymwrXQ@%67a@AF0_tOr=i*v~eJxD=wZP{5QI3eh6@B@gLg7GV6nAU zy8R1u+7-$b?+jXtM0y|~mCCwaP^W%SCKldyZvPItp4mr~o^oXbg5P7U7|fdD(vngc z!Lyhlv$LrY(@5pGWPR~(!wVl^f~IR z9EwThUVA@zU?H&fdNUhdRA~2~H$$?t@3zC3xLNA|rRlZTL0%|7*Z-`nN1j%Ec?oF$cA^h= zznpm3=;cs!72mli&$Gp-~3qz(Cp<%_?$7kT5}#oty&*!-5( z@m`cCPWHXrYwx$8m7i6)eE6BNVC;*Mvfmps)}dP z0x>!|DjTXScXhUEoiFM4DI;^}b3Hx1a^(04)vpq=vXz&~iD_Aw8rFQzNZajqNcxUb z61(GsWUr0M+83^y2!G-3&O=*2bx>fvN}p1lGG1Em=lu&&Dsh!9Z*U-+VzN+Ojo zo=O~d-f@S-wSO(Ftjo$&aUGW*ez!^&jnKiR;e#Lf`2QnWfOT4|rK&Z|y9spBmr_z? z36-&gDOo#c)Iv84Jvlj{m14`2fy%m99iAImdwZLyJHo=d^U=v7+4vo-M!ww_kuJrKzY}Dkte=0n4=gtMZhT? zC`|7~`+2Vml40Lrm9m=+(iqpaYs4C9!sd!?t#HPi2fa0f1yR9#K4%E|r=&Q74Hf~0V-Vc!FBm#@N zO9L_zZv;ywl4c-A#*`=Uwr$yslI=5qJ5Sv-HMgiDv1lwp$aH@`{q^URZEIDsGRgJL zTERQ03l7K-eS&^ZzW|3oc)zu4*=UEB)Yx|y-|q!!Lr!rI?y7i40Aw=dPr0~BGSWv{ zZT9v)&y`7el4Vi6#yr>VXTl^jbNEXDNc4sVYKDhh*fiH0Ro113Y!m28{D~32x z--K+h1V|Zcy&GaF>F(((oV61GEQ9Fp?KIyBX4Bl1GDlSNyrRnO`1qu(tgcH+-*NQV zkaYAOk$7{5%*@Wq{K}fFtIm?RWk8x-7P&jF2%K8=gMAN3o4N;$z^3x$*`aB93~dD3^vwRO`wl$ZNr4Fhd_A=3m}~Y z#K>e?40+;r|209NU(#m%Bny(n5})%|uE{k$&fb2TIZq!#SD9BGJiEGJEK(3*>WVbD z1HwmWc`RjsO_77j>Hm=|FH|_xzoSgK*l-X11O- z!SiV+Hw)xH!X~(e0HXj=reJTmR_ddajXTO+=h{jwbyI{(j|GzpJf~h_fQ6TSU^j`~ z)HL#3_w3Sl)QUio<-j@b`NH4Eq7^_mBDhm&nu69d?uAXe5fuiES5{H27404ErXB9b z>h2JaS@L4rtG5i11zB?*l7(!ft=n~8#Y9>~pP~sWf15|zCxJ?me5$+*?yENc_MTGE zo>8#f4TvpOo3urp)Ve+Xqj`%9Ikcd+wI>3+|CvZ$_0J z9dG9+Bsa2F_uf{OfCVko-7ih|+$v9f?j6!|vV35MsE#2IeeSnqu<3=;e&mG2HWp>z z$SuY#&k-z#N+b)gpVkS@XF8rVQ3}~!rWV2x1PDV*Q@bH|e&j+qS<-RuMv|C@+wmAPZMHK%%fRZ75OPVC>d=y+jF;lx%L6Q)lmxBS(64oMKac>(0*S z@aTwSI{GA%#zNLAneG8;=^iw}H{3yBizH#!(8X7M3D2orm*!I;hobip8WSaAOC$XR<73bq9HJFYoF z4Z{oeg7E`oftg93jlxC)!*?C><=hl`w;MDZoYe{9LqLrnqE$?*{p2}8fcT-r_{+`= zTE09{CTpska31%TwzhW3ZmgT#@~h=P2&9QRwSyV=+oL!bTFZz7gpl_fgv|NDtBdb1 z!sdT(2MQtyls_aoi2PB%C`PyfL1e8v2s6#rP1A+GUSD4`LI&$0^}%`8;2ujV?Xz|To)3HbYiS2)X<>pQT9FtB0g{!}^w19| zd;fFygNh`p9Lijavc@D3ce3Nq~sf@RBMe_$rx%AlbBhP#$xx6J;23<;1)J$&b;Y6e$a01v&uimf%b)xLHngGbFh^N1c<*m z?}!fv?Y3C1_W9)qor6n@H%+9G=hJTea){sD`{?h;6+4DO5G}E@&jRJ&3%IU4X|=zD zYGkb%91%4eQM1mFKC9>2SV8TOa^hf8(vi+`U_8T;MVWa)dtr>_*w(W3 zx(jlSKG)yYyQ_p}7Z+qXzg5+e#d@r?Dxp}hobn3BuV0hw%#5sL*JWvTM#hwM%_-qR zDGtXHX3PPNT0{|Tiy0kXT2*FpY{|9>JVkJ*?VWAuXls(eV`t3u>+36+flR=IrG~7e z#ztgoYFrw6yCrekaS7jkQj#xzuIg(k*&G>{+|>oydT68=BEAe*UlmG+CDIa>SkI0m zkB204R@+HykAzMflW=}oJG#hHp1rj$ORB;qwI|NpFCTmVe~^=Rf1CDqP3yK2htOVP zT#jTp;;_83B<(GorcTxu7Z0><3i6U}R6R=x6`|zkmX~FAZB8a;Czaq%n>Bnll4L=6 z8X((Buqpx9NXR9706G^vTqT37OdQV2l!&K{XH2wxK$<&yq_yYhE=daOZ|Hsy_C%~v z^I6;qfqqNSALJeKnf_c3JNn!7=w;&y?@!am|L%(wN!GJ2x?oO)PNR~nXgpHZ(sWp0 ztw1;tmZcF0lvU*7&2foj8f9xOFZqpaIeF}aIsZ)I{Dn(K!40UpswgURMOX#SVoMwK z!hGdfTogzep%+f!ta1fAggn>7nD_7#fBj1HF7;7=>x8HBN% z0n&#eKt0>ESOt~OO3E?H4}=Em8)Z)VNfT|*1&jb#eX*heENtYJdr1@T1Ek6I0m{ZH zfR<2N7c6(JD_iQt?(1e;&M=DU3I90(BP)IQg`pHa^2U8!oDk;BZC z@*Xx5&uSW&{jCS&71C^7m8rwMjVEZ3s04q6z=NQrxy>xj_qN4K;JyAl)S7_y=rnEm!sj?abzhYer zT)nw6tW8pg;QyWPy}fK=zi3EfC2VOsM=rZQ?#N%Y_wpO4mM$+`p6UTI zqP4A6=I7>j(+Ef(8xljH%0(e0+92;^$pWMC3hr^9NiLpk<(CaORgGLZususTAhXOk zsqghh7sMLX9FEJx{G{A=@;0+s?(p@?vYo)Ct8{NJFB-I;5>c(C=a}4f&vOcAAqg9D z^|6P|Z|ICWDuV$sd_!gUx+{mRPp^`KxXezE z@5+g&zO#2-{|==k($OK|?jG6PaP2E0>t8r0YoGj*WG{|M^zLoxd*fY&->T!Wul$7) zqE)%$n|?+~VT(NU$8VHZf5VRyehtRz%7T3MQ-2^Yd(&UY_N6P5Z*A6gQJTl6uAes% z@%$k2UrLnlsqVyEXGts6JoiGLaCgZASd8Az5Kn%+^-cmH8J=s4^34ipe%EF*okD z283i^on)h2Z3vyJh%M)2YkgaO?BD;mapemF1LXMlxSTmttTaDHMn;Un?TrM)eH{J$ z{ib{wG@V-?V1Ehk^*rO}fBr2hq-vkd*T>cb)@Aq!iWex9D0F^!m35R1l-J?mYdW6n zZAvJadWTV#on-{~vS~v=tVNHgSv(U3-U~lT^t4+8)}2w7G;S|=7D}WaW}TY8dv11S zcVI;6AzdiASW*}afUKdX6CWVfSeAb2tgEo>*iz$Z@Dr6kSr`;5G7n-%fVY-<7x)VkUq|X8s(keiUUN1h9*r!Cs&*n*_)`Hk}7-rOe9=4^l$ zOQh^%hreAqke;A+=Y?xW4!n=&p+NG<{rrw4lzs)w`wQ>G?S#D2URPDQZ_xAnP97`u zWzf;lrq7LQU61R#Y$LGifQqPB*)*Zq4+(5s&dh$T!GBPeDNk=6YDqukiULER8%Xy- zU`ZBRUO4y5Qux!ORcweq*2t{Bn?ag7cM~wQacAYCj+ks<7PdtdH73OTuF(!Ww-T}N z!x4=t&2<;9j{rZ(@qY(F=KwC6(MVC=RKr#sWXt8LS~>Wy^M0?j{bw?4Vy%i-fHm2r zEreUIH{`ok5|&@L)dAORZf=?OPQIKQ=YCO%WKoU;XrZlnN!HZpb)(ybS~_Lp>O~nm za$Fv{@MsA^3IgOTo=7q)EoX1DYh3^VFIx0?cy?I2`i~UO?u8*;N`z4U0q$hYUA?M^ zx0EGa>pR;rJ~<~lsyBqwDQRm?OH*r~tgkMbklZNR5U#Z5HA8W$d~fLPl&C7}mUN+{ zuPZ@Zl*s&-%rk4j)V0Uu;Ya^k+P?9}CHCd_OLzNG={kCQ;n!er&!ZR4$t%C-U9$T5 z&&&3aenXfXvIRYZknYuRvqQcgHXwORb2ElKnRc%KrBy#@NQIS7>UU2W-R!r;}FHIW3GFri3qu=}71^WG7cXqxDOR#7=Ia6(@7>Q|e;gK+ig zRr$G}`#HJqzWYi@5qRjKhvfYq`b+uacm9#Q>}4-ARsqP=U;p)A%kTc~@0o$}uDk4P z3fKP5@BFqr^2kH-;DcW>O76*%C(UnwXJ5K-L8fY)an&j-2qw?#Lgbj$Vdysi@%*B2q{`#S}+PB&jAdgC>E5Hm*jAty{6Q4%Yp0Z zINk;gXtyPeTsuTpSEm_Z*cjPM23@+yCrUT#UFaVS(?&UD zEleqiJSZq%KZ=9&-n9D51wdlB7G;xQCML|Rrr(&@m+Zi06&!n?lM6x52apExN;W`n zW9!+vKk^-mELJZ11(sRP@ecA&Jvx-ne{h-X1=rbw>fqT-IFL8Woxf4u=qoNA)Po}g zlrQ-q6>g@Rwi`1M-Z&df_E^)irDWOm8R}6}pq~dpqj}%qAgRg_Wm0wx8vh}@fzkUTTtgUcMpc5YJ^v_DK>a4%b3l3fo$mrA5cojm$ z9DmiDrD1GBvMQ&YOD3N|Yor39^*Zg_IQQ8O#$oz5ZhtwI=S*zfa^$pGpO)4(M%2J) zD7L2z^4bSBnGp2=!n3`lZCKlER-cvCI^D#B3GWsyo0GTSqx~n?%H@UErPTpXs<+4~9a0YNK=Ht$McI}Pz6{>QJb8+_P_9A5rPxl$17y)Dt>wUb zQ9f}oBY&jXYdNvxrCj@S>Em~M4bMwuQj*cKXbeS7=v>yq93~tA^}_bsQfrk1zxjm# zWr*-W>7&jmC+ZU=C^*Oq^+6>B{btJvg^{#XB3VK0w0O=wyO=+f5eQmCc5F0D4(f+C z%zIEk4H;3P3&FBh0lR3Ny5KrTnE0DE#wTrq>`YE#Wem=f7hD^Z|H|#72z!6=+M?VD zzLzACXER`_!cBe>s$i^VH_b+pDEIt^l0kW?wAOlL?Nu?NCeJWIFSLG{Rd>o5uYNX> ztcc1EX4`AA+M~!qj*%Ps&7Slq%Yf!%%iA9OMti4E+CVG^cinX)t5jz3KI*fW7B85H zKz4Eu-UQSK{f_nvkw*EY?wF)Oeliil@3c=l;ZX|PhJxQ!8TKC2&8pmA{u+DEmT&R1 zT)UzzGnu2TL&wg&?o#DIJ9cD?eoLQr?U+dh%78RhYtN1Vl}F|KK}TnY*%K|O{gl>) ze$os~+t5CoD{e&0Wvs6QxRM_RTzQm0U$mw2khbO|%(s2h8>BUslEsxpX$i;acnp-1 zZp<0Q7ezCy<9wxX?&FgeB;7hF1A_x*62M{l(nZ-=TdI1$AFMZGU63PPDCe_Ejz+Fu zlG&Ll$zoZEhDt~me>bI)hIkPTdS1t^>B$LG4$V4-_YMw9roB^URo>!HGOK-lXL>^9 z(q)NlZ^+Z@`s_KZCK_~WJ`9Lf5` zr#>yY?MC_Vhu&|>c~Av|71Tx4LbABm7EJZRK`E^i0T5-IG&|4+npE&Hs91k`X8Kpm zCrSV_?+pGZ!VG}UV!-vxHl|W(;~s+x9lpuzqvdaleCL^+I)I@#l`0dKXt__yk0OG^ zd4o&%0U^XShja!GKml-yIb}e4QTQ0p%ayAm20==;Re0lO0jvp+TVon(M^Kt<+4##B z(5}2ffHrIQTp^;{hEe#OGUwoHiU!QWmV1Ytfu}F*7^SfsY#X-^pzLhgtfE@1Yj;iY zMRo?*Th?}eA#Yl|&#nZBC2I~@H&>}0d0`$M@a$UO>A&CRr`o;drd_l&x5$dNX~-mP zlRmem3WO!K_Ib<{fN5pQ5bf0=jH25WsO_YB&0rTkd%Tk+s|wqNeO zhjF4=+haM)TqRHz5L-Tx)wF*nS83b4w_IAQ6D)U4;Y$E$cdQ#MH(?iC7g)7;cHwV* z0TWq#+DV?uX{r^j94Hg50om4mge;Ai_F^Yvz)$|n&ngjGkx+xmX$Y2LQ|qF$$-0SO zof?+LNclhogl=z2X+P4jyvO4b$QCrQg*h2ro|R@LHwS^2bP-u^b!}M|CntAF*Jh&< z?xY@Otk7)eXSo0x$U&@J-2=yDClQtjC4cjgl*A&P^59?ojImNdh-$&$@#pG$GV;hn zAC~r}-ikpm9Qu#mE(=%xT84L4q^129k?SKO&1_a~(_Rf1M=xnRv0=@FFgj+`>zq(BUc(C?kU#64H@OE7@t1u9iL%nKEd@V=Px}<1(e~1|qejZF5Dl zo{uSk$|NM-qStlnJxZP&aT-uPcujRw+N|;DSY0>rg0SmzQxmc}Ga<_>tFop#g0 z*REccv0mIf2jr17lQw+sQFc&d{bglAdV~;+4!o@cpPwIEvGEv5CTI$7tNS2lZ&p>0F^tq*%& zvCkD@zgK@Q?XY;Ad!DIg&1>;~#uDn9=k9mniLuL1wish*<3VK0rp^3S_}mMwsYb$Z z{a#stAQet{P_3N!nfoBnq}TSt+Si{{17jU+K4)B|v&PMe_izwm-rYk7seyT*3+ z=IZP3m!`HZNez9yeC6qn%G&TlRV`Bv5Gx4QXFvN6IWqLTng|;AU^RnmrSE*1ES`H@ zlB#3wXu2z5MM>+-(yTPKmy$UM5#_>qCzW;*)9$|$2^r#(P#M?R*1Su62;WXfexmjT zg-+kp@*5wyCM!CoWI`%ib?i>Z($W!2Nqbv&33+k^s+!v$K)x+&Tb$5#wXSVvVCbYF zP{=l;W0|PJ>$0R|ctQ0K$kY1VjEw8w?fTBEmoKQiW(f!DlkE}HKGPA6$kN29j89&b z)7^uT&^9$aH7-lqE|;|(t3Wm`mI-b5Ve~j9X=CFfGCeVFju|CiSgZB}WQ>=@O=_Ay z^udojJ4qISE;cqcO&HHwfisMCA8a@@_rV?Qlc9K{MB-8XjL7!73hVg|>C#FFB3THm zU-^|^5!S9e^vI){o}4z|MR~#VpIf-X!cVds{3Pq_*|X*#009m4!sTIk-gE9Tzd4j6 zSqN>bI8`si`T2zs<%K$h%rIDyCmKFMF8Fld+039YDBATzduREu)Sv)(8%cP1@CQ~+XmcVX17SRN|jUAI_-5A#FBz?!n524j{605 zNT;H6H#N5wf-mfK4RUGngY+UK0Oe}QXdMl{0x094_g1!ySOps*kK#=^Q~&exxSj2F zLvpanga3KKc_yDIfJDec`78%7IdO--9S5D`9-4u-TK(y>_XVfVfm)q*j<$ z`QBv@+Iit*1?;`Qmi}vQGT{EGDids%;1ojpOi;XzziDd_EtGRhELiWgY3dq9l)ei= zp}ru-{8n%K46DqnC0w;1CiHmHL^vr@mPO zv&s0NOXwU|36}lFQ~Mt*6qm zH|w&rFU`-f@i5P_^-0+Bx zc<1M)WqEPld|zK%k(Mr1-rG7#@-TMwiZre*$VP;v2BpMfR+VBDQMb`CM9Hgb)B{Jl zoNHmwdM=eJyQGUFrR~jGxd?~u4HJLj)Jvu3u5Xp|pMR$+-^ERpD`A&tec~O`KXj+o zXKj`+?%S9-C+UH1x%lLR5+#yg%32VE@YL{mY1Oj!k|Rjb)a0nye`-tX=FHG7(uy^x zKz!I+%S(Lt+*ElvuVl?@k;1w~lt#wso`GYg<)#u%GN5H(o`I`gjdkP*m2>;UTD2LC zDVa}8@6pqyj>fNDm9XkSF@0~7_J<~PHPu5}w4U6N)_c~B4wsP?m3@#ZKVsUECKsp8 zmId9NJ(^b~g?gVmVk#3jNIM6A^Xz6Nj|shYRl7r^k^Q)e(KD}HxoiT*;KIyWSv*A` zP?OiM8)C+uTH|A5GBZ7;!^@Vo<#n?*?A`Brhg`m9+cxq!NPavcBr7n`MIFF>5G5{> zjLHs!>VBZYH?IYf*MVw#O%?tH6@Cl&gWB2rawO}*g$wfFgAd9-`{sWtCyx)A^;m!T zkq^l|_uOlg6$c8u8p$FU##`R<7V`|0-@pCrmn4&n$Q^gwQMlS7h-5Kv)Jm`rCjQEI z5#$hbSgx##S1BMXC_NCYfVN}OV2MttGR=AY*QCweXV3A4NP`9U1+7O0MAF90CIb*) zpg{30u8ndQ7L;DpZ_@DhCs{p$zQCcFAkg%2ukRQ%hLns=mi)7i* z);ng)4F4njwy*+m$25W zpFlyb9vC_;M_SuuX?ayPw4eHk*nSbTp39LgTnh=juoDmOuIKVvxm*B^v1=pJf9Gpu zTovo+=1fhb3Bm-yB3yetE{9wf+FqB=j-+HGIl27N_e>&8;QYD2`0B$+!l_jf_p1GMt_sDZ6p87fTTD07BK?cSKq{ zI?bL`8`+$h(3%?`k%Z=>TgMhheylza3|tAdx@2V|XY>|_Sa?g4n^sor=;w|mF_nr* zF0W}(9cyKI#q9sJJUc7nO2WpbM$CR>?wFdJlG%l6>F6Jlx%tHsYZe46qt6QIyHh&e zw8WyOZ4hLIqpP*W9E}az63uRF-6!PFKJX#=!*{(~)6it}L;7%#-f`1OR;54}^?;N_ zv?h#YYd_Ej#F|v;oz2<-KaY?t=C2&ddjFsQl?)vnl-IoGHO57dihAta)6${>_{fo> ze=-GLjbvHZm1X&}Kl@X8_^~JD+U2L@jc@$660!#TB&#@>Rj&`LOqC8|2>5EVNRSl> z7VC7%foCuqX_eGsS|~<;V5RF9)kY z?QQ$EZP;`Gln+xj^k0=ASpKl^k!GHeNK$6y1A+jTuW}&2F3)^I<{&R#_el4)fiQoXbLL<3 zw}Z8yllRi|rLe#8eA*P_5OU!#qbn7Kr8S?6`!Q+f3iQ5%90)TG`Ms&$FJ3AR3LTYe2RDYa%{~Ui7eZkG#Xu`UCU@}e0bNDkhNW- z6W0w1bMOFw&`D<9;lveE%E#Zi_4Bf{%Kmg)W*d_w#xB;8h4K>CeBm}3OJyXZ z?a&c#cVNL|lVbD}FUi7kmETyEouzeIT38S^+MZpQmAR=gX)CyD&gl1bZOgfxE&2Vo zzf<1+&Ucyf)kxOO-6sn{q>tFz*ry50gD_OL@Vxy3_rUC8RyJ4n`sd$v>#g!{e)!)N zz7>JsMmX5P$HFOTl#>^=TuY^#KY!jRq*$>0*MndC)wjyy=bqH_)Qgqo)fF?Yv5Qf? zVD=EwQVEt|Ro}0MwBjy)+eE zilz5iC?>ep@Qmr{DWhcFEHFSq3~iWjT(L?6dZ0w0EVB74=goks1K85yd~;R1$2Y4?1}8U@T-K4x}S2|bq1O7OpnHdwBnK~HbLY;SGu?h)pNdzNd*ASfT8 zTw8bVcOcJx-LYKS9LRfTXO}6Ha7_DAC${uDT+6t*b}^A%Csqa-jU2o z3T`z8&MR|JL~Z+t8{$J*p{%fhEc>Q;+fXzWm3);x$L6c~&GMFC2couZDG_6t7e=nI z{(+?i*GPZ;`ekG#peS}|U9%60vy$&nXWKa=5BUzk#C@+?8oi+J<36=oW?g8#KvrZ_ zmXS`PUAl>-{epVLs%JeFlz6Bt1zT9aR7=JJRt~fm%GeUF(z14h3V54UWr9_NYhNvG z?GlegWMX`x@XO(YNfZ<+z<9-aHpU&&07$b-PdT`LP)<&`w#ak%)4Ok*d$zNlGA8)- z1lIjxNtjUrP$?bNFfx3-@Ky({0k-WG9qDojMr>ECmzd!&;Q zX>XjB#>I=~d@W$(;&qh;&wt4q3uo$s+gt0-`+h=ZbE|tcASUhes`TG^s`ky4$1*dr z8rr+mq$k%WnN>TP&$_RXr5S1Nuw;VGgH!Pq*-<@^Z6&bqq%um1!*N+#nw0saHM6A% z`<2CX_)11%k|f-DLy-c%-XH9_uN}JE5;RYQQL0w(PKugJMjMI-fqdL939qsTe^GY_kREF z=5Xj#()R4xCrfXyHPA&RG{(Zx7}47Gxtr7t3PTg)4c+cg&XPr0_y>5-arjZowfxR{ zEUaR#%>8hdEbg<)SZ(=+z<8Ag7VR>?R6?x;3ouyV(!)%+RWi$`%jUWGyPI|PoECl)MzlnfJcZwq;3Ag#;>4xTps#jP)tAd!I`>tG&;`WWRvg2o@;b z#669wgfqxF@XT5j+;Xt>;$Z)#z0x*lzqDfr1vA7A;BGNoyJ) zU9=7IUad~Cm_S(ko&x&??GO?`zl2yG#L5Q9C(pn-hy{W&B~R2HX=f~OE{YD6mCH6F zHmGg!-5u_CTOFm>m505K;2%WFL{1z;7FGgd1{Q1TeoO1rS+;N?WJ1qQP?S$MZK26n zv1~LzKdg-6JkM}tV%LoVzT=wh%ANH10lDWrtRp0C`$btxmL`qoyVO^+WR=6_+LE-l zcF4Gv#qs0E3ui5cFJF+hT!T!kFYT_oYScUvECctplRceA3E#+X%8ZWfmG;o`0;QO> zLyamU%SG2K2kv$2kFd~izjr+s&uwaqOOuE6MzuX$U09ZrXKt6qbiXtv(z3HOQuwtp z2%Lf1|Ea%vo3u2w)os<{dP2TG@|mBPxv@v3edy&SYq7AdbhP)F^-{H<9KixBp=D{` z=rDg{4a15=8L#d{CE41gda+1fe^-fxiimz#sG78Gw-zi^tby9vS(AmiWf_?r-F4B! z!lW9Gu`C5yl^91js0&vw?x4PKdv5O;kQvo`d5;&XO5ApIJgT=nfL%+Lj{AgbcgSkH zUC}hRd#tk{pJz)AYwcXpd;jo{|I`pH@Ay;Al2s4|Hy#AKz=G9aM&$jY!6^3O{jbIH zfHeW}=C~>Wg%i&(Dk%p{6@LLK+^_G#I5tFS_WTs79Da_*C)59Om?RI6*&!_YQr zL8WI|f>l~Zv;jX^^tYp+cI5}5zsWykwja=MY9~p-*w1|^a1C|bXO-s46;=yjp$%cN zS(uxZxmgz2Z<)wq&B<0tL}Dg_7Z$5s?a~7*DE#LKiY|S{uDztMY?weo^KWCubPEfbw_!32ECnJpFw07CTI%>v5`(Bo%0u}lP{TouP3hhDGL2asn9qA~I8VFdA71z9`ni6K+6$h59 zkX}DEdR?;lqCT;>l9OC^-B`D(0gDP4XzGy0^_(G1?p&>SVCamTY;Fo9ugfX}zI^>5 zB?aS!Ga^$;u-I(YVJ9^t4JY4F_*NZVM{k#xf5Q*T7yscy@(-W*ZIxqtH>|FP7kZ9oXLOhi zZL^mi{fe}yd_1DE@uYb@m@M2Uf96rbVJk zu%Adq8`^NB3V^Zg!T&*I>;7#X5sRN!cIDIBjkPfv}19@3c(-- zNH@xm*VKEn;m8&iI`-#cM%oX`nozq8hBkuY{@_*68^c(dGrR5^uHIeQlmn9$wbq}P zj8{W7rQX5m0$lDTEqsy%2$r|4x&!#n`HyNqA*nT;UbyGmbRDjOT?^=Qe(WO~yYLgN zTF48Qk(rq}85>D(qzl7oeZwBGuATTpNl-g+7e3dDt=q%PyON z6V~352+GVxz}zs(wj*4)uOSOJH|_poSk;g>j&Ru^3Kr=k;nxr5(&q)F%B(%p^eisU z8v+z0Lu!GSF!{lGJO$?GXHCAzCkx}dx_e9>sW#URAPe5ZPqy53j#xNw-u>qO=J2P9 z^`IzqHv`BOCgL2~qMbpuXmeWuItB6(f?m$Go32BiogCql{JFduH!0JGtkt|4X#ZGz zJ330&G}Vf@5-HZ;8EctGUTdl}T`Kc5KebMNmBWsfSt3)q4rpv_QjRy55=dMFZ|&%az3HT!)o0z+K3Mpr6s%L?S&%RX z_go0n?8eUS1hW@wO8#0C9p-oYJtvDz%}TOLBMi>ym_EI8rtqygQt1{$v{tm;5EKJ9 z!g|3oUi}R}A|3Gwx%9CgkjvK|HLi_b_$^he zFr*Gb#WfStQ-)BTyZE?qEzBn(dkB>yQ-f~7xo;p=H^UUDlg$r`-si!r|`zw1~5ty|HJfPbAAU%~DDr?6eE37M9Tf^N=2MZm%(#fpmP%W^j?B?7Tq?Y5`?Q*#VxzdsugsSK+}dJ<1~Mjku2Y zqd=ex;ugfGtN-rqF1;2jVdWVw_r9RE6NL8N4PW=J^@0F;+p_n0-uKw=hZA)x2((?I zQ(`eW==!*N;O__C{FX=YCX{Z{zO+Otpsle; zIIqXXvbE@vc6Qd^$ZnWue<-3h_z13SMTNBz?ATN}&kkINRS3)5?CkvReL?qof5Y|H zxbCUKit2sV4gdiE^hrcPRE2@HVvP-Xqjck?zMb39zLz#@rw$9q3}l3}exn#-Nnuh3 zLU^MN!8po`!oHgCki~746weuKe)UC-oLozkRCILM&+qhXu#)E43-| z<_&z~Ch45Di?IaR%%p-J{x;~(r?VOYo>LEK2c)Im4YP@m#uy2a!UYrS2f%^_!7`Pl z_|PBykwgdBB`K-)dzb4W4UgOs2U61v|LtmEL^m&s@tCE&RW6`Pwt4nKg zePuzFXj@Lm4%)o4Izh6&{_F$hZ*XpG%qaI*N4$VsLB_C3oj&=T!dYwmaqF)j171J` zu#WNW)>y`nxX0HvOV;UtMeTc+E?$tqrFof1T#++9$0V{@c@47{ShJS9dZgj%Wf?g6 zVmW(j=~h3r!V$Hedwx_>ktRcYuC!h-=N!E2tUx7qL69Enkz6P#Cr;feSFfIz(Aug@ zjbAaAB#4d;6+dp|U|h*(BA(hMQVy<+^LPOi)?HDaM5&xw7~g$Xy>RQ#dd}Pdgj)U)dPrMOYe|mwT=1fIy*Nl+n27Gzc>4jOGDDifnWWFx5zu+`+oDc z#g6fMAUZh{8r|K471w0lT#{7)#AZ92*NUlKp+Ag37lF_`BA7Pj>yL1R!Vr$cv>{HK zSuq4-Pfw3g@DJi!4gAXJK~YIbW)QGxW~R4rvIbOAru`_$cash;%B9Hx#SN0&a`VoU4NIGEL~CGZ3JsfX~;F1S93ym{Mo zP`$f%Rl4RdK-n{c4q0)^(9MAB2f+0OZ=D6BcoS9`LYh}2S)2ovluSRtawvCwJ+M{) zB`K=lCF3z8A8{L`-4WRe(&G)j;PMcZ7k_%30u6%F?*(lirIj^KD=Wo5R<6uAS293q zg;C~MnV7`{46sr5TuHE|&k_Ur&_U!4WyNF(gvtw)P7W+vHXahx^MScO;<5(L)< zc`l^WYtceZy0I!K&_yj^OeQ!B?zy#sF%K{%U=cNu5Vif9v4=sUAbdXYH=mH)rjq%Y z;gohPPhKUB4{7UA=N`enC#3KB0O~(e3cu=l(&?wq{H~i--~>+yUESX&LJ2 z*tKARr&N)Tv~){meMQ@Po1}eYY+U>2{E{VI&T5rzDte}T_R74=bxKflHyFrJ0@`=Z#v#Z;D4{JY|yZA{tb;s$th!zBCEV3no z;eHvp_!)WN<=-#Bf$3rb$5ss#+t+&1F59Doc`<=oSUKEMe;NXd8QQ!*)W(6a-1U1OE3WS0jE(;UrBC3UPxLzc? zh-G5haBLX&T4C1{G#i3N;gXm3c4n*ylfC~LIpkmv!Lkb3LCT+sFZ}5NiYD(w*#-qU zTobH|q1{KSR=5V}Z;0i%r@TbtNr@-)(Ef~kDt%XV(C@5_D7}w+0^(v3<&X=XWn$IdRzbVn1CQaTBb_*PuWbwSaM> zNy|Oclrb{C7PxVkl#yoSJ|Kyqa4~A}E1r9swDug8Xr^iRxc0<@GC6!n=Ekqf>e9T# zlmt*74vQ0$(%j!Alhbq3KTver!^>|#sF0ISf zvD4DrHz18Y{W86>Ad^dLGCe;ld6frg&1Xi(AHuvJ(Xl(K?Ty2W;e}ZfG{FIKwJ<*; z9jfHJ`#PGNC9##2_0_D*&QF;@2N+sI1UZ3d7O<@Mom7GWE~>uPb^2D7Po<>qu}2R=sL?h%z(-cHbAY?+uID`^Y0BDbBBP(#GTc3{I}ZyO&Tep=!Ky|VL_Mjvx8=~UFErJtiY_jJ_73AG%&0o%)%C* z&--h8E*2V;0{l~N6v!r|f&qisUfdHQRV0zYjDfkYuh;zT(v5P*Ogf?657PyuR-l*= zMUb}OElb)MWrgCIQYDS@LuUzT`+>AGNR}&mS63)J4UJ)0(@xC*$H4AjnnYRHHbdJ& zao}1McEa8>Q%#wn-=J*Jp2-7BG1gkAfMC^TmKRiNBjhcl`Qf>K)ZT7uxn}<$Uyw?y zSO-VX}wi+QW9JY2O?uG{bYR?s2L=Zz=|L$f%`Rm&Jx`<^GV;> z%4t8;zSW@eiS=eE*z}Qd1Fsh-;`Fz2dlH)QQPUPrW`wO6sy)x2PcEm<_qKx~A@!c4 z9dHhri-jHcMPwB^0O{0LqG_@+#NAgJm9L?U7(cQ2ID)cYc-t~4O(X4|vI|ZReG^&7 zngDe`iodP3RlDgm@|yOG+;hsd7o;6x#W=%tu5MhN9t1oytYaQgH)|;uN3zH?!8)|& z?N`PZO+QgS!MAr0}!Rhl}xB-7h3ja}_BIz273 zs~a*2>B_B0I#KEpXsleC4i_a5K%ic7;_UAJTOgvOj(feNE1q4G87)hQ6%hqdw6`>m z{LXWwvsX#AwN^PidG4GNT*ybM6>_Ih^VoBX%s+ftq%~yrxvB+34s7c>DPR2ff0Mgk z`->9k7?OuS^=p#NE3xb>T?1AP5VKqFc$H*l#$^1H?~vG$`z4eN88QW_f@opIf@JY` z^Lneq!*Oj5sy_`Uq`&)=L{zVyxIR*19XfaUamnp$OK0EUZaoG8!n!a!RkD{>IS_FW ztD=|S)(Y;S&KeqHGI(rAu8)nGNP`e9A{g$6WHCt})x3+A2QnJ@?Jzz$D;=FZ#$x$f z?|;Ai_S^rk@Jo3h7oK&S5tl$8N0h*r5-q~$8sg=_j=npdiOY5!ZiV{+1;ftfx%JxK zL+3-F+0d*gz)=&?ZDBFkhrJfq!2lG?0K&s}f4z}j4tI~c&VBmfzu$qt3vO_5s7>j! zK-i+Rdlmh|1Pz!OPG)mahADGMhNe-x z?(5|TXOESke5_?;aB2|;9T)MWgCkp zO_*%XQ;r-wmnd&6jOT9~YMy>!H+Hp_w}hlKs&EyoUygv;wxi@tl>&r#kQ<)2eN`j$ z#XfQPplvXapg>wH;LU-*f1&hMD1riPL7)H=iX5wReQCC&%DGV4G`PZb=lI1>YU(VuY%{@nG~7t+qjt>u zky*5J@*Oljw6?VxCAN|rg=o?L=zE*06uY|ggR&`ChxUB2{9OO?KCASc{elIHi7n&- z{f5cJq~@RgyQS^0QXh7p&1v!r&o92ufuTQ3$^94K{v8ra zSc}KnlMl++)R?53+l)LUFUok8&cK06Vc~wNJ~?fj;E?vhm&&Rs1hJYJ^i;DqSZe1VmbZFpOq!O|Dn(Qwq)X& zxZmfYvR* zt?$Cs3)=1pJ8$cRHBqw@BY{MQwnX{2G_~(qz=8mAo0=UrZE3&23K`CCN~*O}RyV^_ zW5FE(L}Gf5LFvZ4)`JEKr^1p~xs=aUcJn|8QzooKqEHd2__ab-_M^-*2m~p{Rsp5b z3MVR{Sh4n#d!6!D?tTUVTtA$G;NVvRE5RyF!QpKW3M^4}Cww;1En1@(NU%`RA>4Te z1@^0H@8~i!y-tx~AT;|$=^*1LFE+&Nsw-E2egaOr<(*zvOMWljZx7lJBe45mU*m!oeZ$=(Qmah@EZUZ!o5~E&BKDXer!3^PO_NHp*_#f&(&~G5xf(H)0MR= z`y%Y`d>T`Zkek@r!4BlyM>#N?O4xR{S#2e3ZLwzyL@RCb6rA4bdEni)p7)le zYZ~^``&(h{a$)j?U)akgV=VHJ5y)2geq`G@ciCsUcIMh5o0qq=DB0Aq^G^EJ1D;iE zAC6c7d%pNvd07sNdVsd$A zLdl%!h|!QNEzL=9x>eG#qN^lsgDD*sXS9DPZPk3N%J8KpWNz%5EKX0zj`kJWH9+x& za5>Vo)PS1kYO*Ob8FcEt1SyLh%6v}vF2 z_`eArJ!cpGeZai1q_s)9x;jlXFZMIaZCSU@dVt_}!hbKpDn+dXWG`ZVGQINfPDVHyv z&u<*0hwtkf>qh?V7yi7ve7H1FUX{v+2~^65h@)OGb}&HzA+83lyAi;35qP8em4i3X zhLODmV(So_95;2C(KZ@AdQ2Ku_cmm+-&O(TbSl!+99JoB=;GGfZZCYZ*v##iNP?O6V(^G+h_-e}%bovHEsnf{aCliaMG@F%Q01Z(J)TU9@rG~wAh3KKp4 zz!X@oSmy?rzw^{xGI->;iOk4(KZx+?qCy4%D;CiP{gEK+g$tOGTU(I!&R&_QapJlX zuo_-|^h@S%CN2Kt-G3r~^QlkBCqDTJ`L$pB-{$;b;#pzI+J!3Y4WR~!H^;yw+#3j1rUn|$|au$3tY_HdP+Y?8b>ANUazR(nj+%ysKm5PnM|sdAN$8d`krRRjCbixh8kv=+?3w&1pGT~6EYgR# zZK{kU-L}rEmx~b(y9mCXXHgzOzq{Y;yO19?R5@b`{R!)qog5&!lbgXfZW zTrRy9!g>*$Zrg6EiGHlakjHXk1a*f3TFHvV?>7o4gsC+9%e9y?_rp6*xb;`amBs=Q za`+EF@?(-hXVE+~=rNmB;-9qX&Z^9AOf*YU$6$weAp#pg%GiM1VPbqt2Ez6?tX6g% zRJ-)F76V@R5pCF(vDq0DHF0@<*3?v!-a}x7rj|B|B~qp>O<%t%BbT0%`HAbYxw0sU zrY0R}@)DbvG(j>B0vG6lQ1o6r^3a1Nfi7x=S*O)|pNv2H4-#%^u6do73yAUMkNtOJ z&1!BvAp`e*hded<6}j~JKi#(r;ql|Q$;jWmOG(wT#8S6N&#~h&HZdlt+?G75|80hr zWMjEe#zu!r2o@pJ*^8?)Q+#JC6qVVfxxl~`tZ!oz>S)0N=Eb;@wOA&7AS;#wE`zj* zOiTN&%iz2cc>-h9yVi^AL-DYD@*ln=58U@0`O`mnuiSU<3*=LO|7rQy$39xPsy-a5 zyI`_6BH=_xVoh-gE2#-38qD`vk5DWm+Z$C}%QTJKo0`}RG;~*1e8Do|3RMN1v7FZjACC1lz}WnSG(nn=Kp4*tluw;6fj{(Zn0@YRUw%1d%3~3o?Zn zW0ZE%L|rqO&~_jO)ISO*GKYJ(c6@xi@WlFIbq7;06+v2WL_MK^W623pxOu02Z~64O zcI?lCRnVQHZa>LuM@P5hwS7{Srs^x?y`DT1MG!akddaz+@G0z{r0p>lX|(QKnpg;1 z14^VSnXHw>T1Hzf*EW%DtPQ5GxiIpT(r?Fd`!|oQR37#>u=jUcPJ7SeE{PmwEs)vR zQ~Q_QNFMj+THNS~YRI+{X>)&&2SaTL&J*V=jZ;jZZfV+Uk=yiP>cuac(Fx21(+vdW z!aFEme;Ij67V>06+Z1_5v9z+!;(K3oziBLqlP6_seqLtt8)hvQV8XbW#s4ak91^PQ zEiA1`b4zOpi5$CnMY>d1@}nHV0_k|l5Unl!Y*r#SKR07GIBuk$>bt|LyCu`j#uY5P zrgBf^gpvKyM9?3qY2s4=i~0TA5;o-5y#^tvj6%cAja~gF`1Z}ta+UlB+9JQ z`tVOnXWs*|Fgq_pXJ09OcfC=jFMmcZJo@2%xe)dryv*sq61SuT|0Q4ZYWd-R`@^!hydtr5Qhxo{f35Iq{Ww&T#iFH# ze50(+t!QCr2hQpPRbX#zAVB7%_#~f^$vC&-`=(t z^h{7LXag>8)FsioYmxx)UfKc52nr)ait<1) zM+rloqMV`3;cCSi7ZhgNq_s3#3#^4rS2ATo%^qa$H=Fl8g%SwzaEL{+3)(t^QBXRH zbyG_k{nxnf_LJvWi&4f{{@6o~JlQr;O2+*6)`B)w$Pa1(WrJh82i1xpA&p9)i6jT| z+9$Deq2R=|9Yw;(H2Sc&6YF@joi};pw@O6DjxF>-@@Vtv$|4B-R<5m7x(^_`iEPIj zSuFCT51ELZsb#{bkxh{`&rE*o2UfH41Ey=1Z9G$wP=?iP#Rwb@u zcqY?gNEeo_t(`5ITV0Z?GZQAH`^x&Jj`Q)F)@H5ca}@(!qynrp^&;HauWh(lMjn5s zv>m;#=5<;wAVxCWD!s?0yS<&XOtgoy~1LM5PqWqRBX`9 z?Cdn@+^BV&RHkN;Lbqg20}hjetFkH z6r^(C*;p&QA@Tj7jo}Vv$yHR3h-pXjI`Hu6?xS2$)^VSyXI9q^q^0jR?b??d2pkG~ zDG_HN-bmzpFA}9y+L4; zT4wy~Y&K;GyVwzwy8t%9>>A-;pan$-=5g8InfAxI28qD_(ZEMHs^^7xUITe+7o&mOKY597OPq5>?)pT!Z|!YUlQEn$+0mLWpH&ptM$JwVIA9o ztXfxRCJkW%qz~6b6nGAFEm58FXj`x9UX2pj-r0>Nh@uU#B?6wip5MB|SU;;7QR{36X?Q=O>HY7kG0Q>P%w%jK(|l`nqqe@ROd5diD65OF;&g!4~+K(-bxNuuMPT`LpizX3tevTjbYXX@Ve zOQ4AjC8+F=boY(P`w6W%QV|hukS~rkCN7npDch`VqHNXGtTfS8u zfAaA{3igjfC0W}r;3Ej^UnQ}tgv<<0%5Q}j9(`q1bhfW zr0*m$n;H4a%hRf0ljLYBsuj0w_VCKYGRmAMj3A{fGEHeoCelV}suhfLlwr{ND>3d_ zB-o#5OSCEt4t`)^!vZot&t6@{`<%iVta$DZkOQm4>|gPtT$)vRpq)WX_6r6`=kDRi z6XnKvv#v!43U?kN=4OAn{xE^Xg7*dyPx1=6!o3lt19zddwG~rW430tdjFQ4LAzQT) zdaG3VpS8E%8F&d=;ZFhNT!nPRiuCDR6|GumKjJFBk{ zXshhiLjS1665t1UwtAnbtY;KVEbNsRq{{{<@HE8(f&4m4HpJA-=9}!hGAR8l{jD;X zK%h?^jPRBR{Sm@N+hGEP2_eY*mXZ@T6wmK8N?er=)?_gOL)+yZNR78I1M1#OlpLY; zes_NdVehrtdJ9CgR$#$`yfF6QE{c_kiL`REmprgBGuQh`Lp^YLCl8h64sN&*71qD* z7j6=a=iDrk1!yDbjNM-hIkU%-b>W2{Y(uH;VG5Fs`tX47Ie zzz0|r=4YnNx-77|wk*fAPSOo=2{$USi?^6fg9*Rh*bpfR;cnLIXujR1!j*7NmEdEy z-eJZi2LfF@tx9%lXMLn)X5_p~k6w`|eKQ$s{b8L}^49z0+K1jHv17+-UZ+K*z=`39 zv%zuKdF!QelW3M^9lLYd_BYm7B&K3yN5|k)L&C_#j#x^X+nS`SrBAv$x_1ei z_h{+um4E-^KOs*(^;F?a2g9L~tj18~LEgyWZ58?j!i9v>{|(_{jn#fg7BeZE%9L~K zn`R~~aIKX-yO9N$yuX4#tpyGGoQ;JI9o&dslTcwYwky0)jypTLWU0d9SUs4V5Xj#yZw?ZECF_t;q3zv zH9#3M$h5b2n7lh>l=FN?xudMzz|O#r!VK}MwT!C)g&gI8w&Hamq3@vVu~v)05TJA{ zE-siE_MrOrf_6Y45Ud~N6q8cyCDS~^`yHU5+4V7n6AG>iZX@N=7`!ISMtHRAoo)t9 z_6$g;O)KY6vPz4$_ytQI1c~~hjz}*G7w)IjBczHlqK!jHQJ{Dx)+ox2^?h1cZ@v15{|VP1vsq)hWygTAzYM$ZU5f9*>Z44Il|=H zfVa$w+#&d$a++A{98o2;wP&#K z1gm@DDo8Zt-u^)&BOS)CUn^OUwUOVHaV^IblUMpINECl_EUv9cBD`(ZW6iD4%W8vN zX9FNuv|AKufF+_Y-X_;q7Gy@#JhnV5v7Ov**meIV!)*{U-^Rs$m_QfDZ&5;8dfn5H zJ|r0(m%YSz>eAz~sw6JfTzcMNS&8H7+=TQD9yhL)m97O4t=T6ZlEn%8L&5Tm=R zUmpCM-;}d2{%#o?n~)3p)O6DbE)v&z0 zAQ_2DM_Z}&%O9%UY$UB@9hbrB$q6~x)u;WpRa#Qb#*zj(#08afbFbHh(TiXIt>2ON ze&oaYy9y_c1ELzqYUt_d3lu}HLo%o!+`GH`r7>E4oyi_(zlDX%pj3 zO(2B(wvx5H$NdkBRwS-uEnEMrZY|)QSjQ64l&mhS$=uwm8PveQz@Q;4wa&2FIxJO0 zO=G11c7hdDFDeZX47w0D#kw{&HnvN&m?VH46aJdnXJ^quus7)dOW+cB?jYd33@l4J(4cTL z(4e5Bw9|e_hf~PwMQ2yH#B_i`(XLiruFVm^!s!kEHV8U8yG_V@?|IUQ;^l9z<=QFZ zN|0{S-Q8mb4hs3Y2m3;w}9q?`6YKX>kw zOa!?0bXaip1j>~!3gG|G-k(OvmR56( z%Q@ybdjIyhC*sDP}bX5w#~h}_q;O~1KK2QxLhVeV`cn4cRmV~WmFmZQK_&QYsp(ngvZ7h*q`J3_sw7+?64@9-Ux#j0e zj}jo&!-P`|Wz1h<``g~WZtmZ|t!m4u9 zg>4kqu3eMHZ8su}1KaR;+a!UiRN)?|ZRK>rt+Xi4MOAn3=bGU^RsFEFSuP7RWzs7Api~#x{0)o?~NUQ`5_N z{$||+bkY<=;04zpaGPH^0Q;=)wxd6VxC=6n(>@@T{^z;&y=+YKY@dzr$DI{FckAA_ zs)|UVC`TbLzl%~J{era7Pp}41Zz#^k$H!v@n);&tf@QKi@Z9icGdNdp zi>)j2Y!93NX@s4d2TR-*zi%EW^K$8Y?!|Fnvz*YrLLEB`pgSLpczJf#XOv6oDDHT$ zOwcBIFOvhb_3iB|>WasA+$8CL&eMR2Fg}sht~~@NKB8f-w{1edd7tqSR_^pQ+BB|4 zg!Mv_g02z&97&Xb`!1YneFysn%=6SNl{Z3g?&#I8h!Trm!D9lE}q;xrn z^dDb4qt4hb5=GvxtV2*y#uZ-L=y$G-k`JCaul%^DuZs8SYjpXGJeqzGOIFJ@Otz(D z93>BzUw$^Q3uM!_2`LZB8)c#C4@rY{bGBu~qzW>NYq^KK#2H(87rGT?@4tR-`T5Sd zck;<|bZlQj`5hlM%;x6iOiR{(ByCGhyk@!!mX&|ksf^B5gcg|!gf*XvVnDo_bx)R ztOX&b?imcUl&NcHq**`C2|5pg0A-pobH&~53o#L;6q4r@BR<_an>ckl3k-JD3-3)N z(;{5DN-{VQ#ODF!;z%Qwv03GA%aBZSIK|4AuY>exRT6{b(%wr$^<66`UHzPfHfq82 zZ*CnNW$Fec5dEC9fqsTkgR;Ox0^zKgNv54qr+fn11a(arazLa=AARY;gZnxN(3j!} z{EG%fM8B|Wv6AMbc04XJ$*%rBAy*cT7}@f*@PA(nXT5JWDC=dofX#xO)$ z)wB1#30%4HtfEve>go1LA<%@a>NXDXs-4@B4^bhAkUM;xlqqP$QTOO;i11Dgk1 zPa$qgWAYT9ZwMHQFBKH5XE%tF7KqKW?|s3%@WGFmn{U1@zw*3ueHWDOk2uON^Y!l@!by_;DfT|cm1l6BGc>r%Ci463RbE1{#YWYdt6>G z$}-{FsRI@q>YBRexB2>!<35$PA{Gzw8t z-FG0q+K;5qk{==)(nr}4nf5aoKH`^+gKbN*?_)9$B3#JZbP$CQ#j(?A%HIU`u&(dR z1a6hA-KZ^beIT-hGPmP{wG^EdlfG^dY}){mk0lNd3EmByJm+t81(acxcXGR*XxWKc z^b*knL>G%|QHa9AimqIPm1v^qN}41+mq)a$WzG>T9WN#Oexks6%00Ob@;u!4!oWULqJmA84Q@(g5I4Z6 zh=IBei_M5*?sKK?gQLF57KG46q{X2x9gFLW*@AiQ`H$!zv$uE0q>~x*mVe83}rgbUQpDHEni<|6w&}gk~LMA-hJH9dHXB|7wZye6?_iDId7SAWo1mV zP?njMLug!o>?!l+>#yoet2NQ$)^d6wF7+DOuw#PfG8@Ue=3#SlLltC{UK%2p%Krs` z0SC7|lsc@GfGnec(+;j&*-;B6$~FI#9}X*ZQ%EffB-;yrRk)5!bW!0KSH_1EobqNM|ew5RZU8AK-cbB|S)PPdn^ z2!4PlRNOaUW9TU&c={PZAZs90Od1ec8M09*R!m;x1;RNYf^;PHNxMUF#MO^5yeN?5 z$B`+1pVh``j~Pgo5SjD#g=GWlD#dzq=%R#DF}5CT9ohQyyMdloW?dh5=chlzNJsGb z%LIKXEo7K5*J)(s_|hDVs0GVrm?P z2bt)I7X$|~MmuBHens*{P>QwHbtPcMLP_O3{Tbr5Nc-U0IM-8-&Qg!d7JWaJwC+KW z70Rtz+aw@P zJ|H95ud3gfR>0Hcv=S&lw7}7T#e2d&AzxUfqe=_1-K*El=YH#7s_7`PF^1sy zLF0wMg|vl*fRe#X57vrVz{I&L-0O2Geqn5?OWQ3{D3q){q)m1 z6X*yM*WSB#SIeFQk z?%g}%yoX&2Tv_p1C@_rFMtj}wE%V$3Q)8^+t z^E2Mr*%)+j-?w9sADkPBd>;<`Qs1Im=E~akf=G1_L>bu-F{f?Jm{ zynZD4MxV4U;+fJ$=rcNvV>`z9&}-LCer4PH3&JumEz#9FE%ssR%QJIAT#LFQn0LW&K!F+a?6WVJn{T~r8cpd$33;#N@#Nsh+`9RO zru%*G|A>Yx-Q2!ndYz`(-n}70C2x)o?OqzszUKv#5h2r(`@iG6zfS}=3+jtz_Zs)? zn1j9hS_v z(?0)!kH~wkY1usaG+{au8i{FbV^b|z_wU{`Pe1p(JnuQJ+{d4MP6yFjZ+=@z0G2Eu zx9fTPeK`aRpfvTR?s#_?5O6Sj+D=$RJkk{e?d?>fiGaO-a;?qV^u7Fzz{GtmsO&QU zq1ZE%;sS|~7Y5;az2SA3f&T7(IL1a6vqr!F8; zSdA$^X0MryaOLeU=VibtTh`LK+-IK^6zAIdhNfpaD2!FUY;n6XM=5qnnRn_D25BcU zSI~O84A2h!Wy5z!K8yV6C)ypx%2_R23^}mo-K-rEVZAmUHSI}g}A*Sh6SfY%3UfMh26 zb?J|JlgJ7Q=vJlgHq0n}nn;YKzvZO=B{K!hccWf4T_Fa8UQ_$3hA@}1?R6WXuZKsy zq?~z|1Dm0zbBpb9byg&VxG?#XNEP(^sNXgtsY5(fXrr!A&?frbwp=ImKI}<5lCe?p zMj7fAv>)$UhpyE1D3Q^&#Bu^a8H+H~vgyh19BgRZX*IOWGLmnkAJ6KG-aiz=mzJ_k z35m-UEAl(Rl&sEx5jG=)e;{f8<$v&NGc8#v8W%-UxR!=cmlv^q3M-NW?u3L`kFq8W zMA-X7vOA>YIjx#>s^~sF7g*G=f(^PO-J5GBy!7RyQp=?^ota_4aHyReoB!>1{*}3T z^DXa8G~N*R86X7F@XQ^!N-o2$tC{~6SzxqY<*6XjRYv2=4Ju8D5KEXC$Eb_lD&->=r zzGz2nd5 zFO+nYY5e_zc<|tX`u)56NH2pqR=Ts`;Beo(_S$RuE(UW5(6ZpKL-$E~iGYd1hwCFs z1bIQxad7v!gYlKRqmBB1ckqArBkH_Pri1`NMH}Zn6eZk#*vIV3&Q&R!4UPB*YPG6v z1bXk@J+%Nf8iY>2c*T`;Q`e6Q!2+~J5LBS7@_!muFntn?T8IOl*pO7(rWLlk;IXXEp z5B46IL+Pt1`^Z;ZhZy%ZHrLJ8_7$}fz~Rf+gJbPvjdDGGW$xE3(6QeIgTeadcZ90ZDU5S(+? zqcC7SDV0~v+SaaF*?!FA3a81=B2c0yx?T`W<~vJGa@di|wC@@9PeiAl&S$&2r`E7` z^`1PhB{w%rE}bx&J2%YA)>XB*jFNfN5Z$kKv}e-!(-s?tLeqT-Q6Is&!n)Kvx}_FRZ5y6-3hN@okntRg7wyGbqw*##dJqABhSDFg_%%e=piJ5# z?=po=ldbG%c~P&~!m2(4{aHwa5<1#aUdCkF6@s%1qQpAj{x|4_n4X11VaYPmPty6ck}-um15(zlNkMr6^}K+@bs9ZkpwgJ<X)&%`Io@on%HEg8+rhkpu^x zkDFkzXgd%CA@D22nol{bQf`k$z(5kTgaE80xCoN2p!LX)D5X6v7P%j*9hGsLIcBeSPOYeXU15E6r{ zk7P@)<3LHVGc5L#-zX2PcmCfU{B6X+zW4Nb(iBz?c4nKc1J=#etGi07aL?r#wd#>7 zPxtQLzEn~b1e>D{=P2LH#Jv7xCnjWYkv@f0+rgDDN@y5acqfwrt9+h5+;9F*KPWRS zF1}{{F2bPwUk*pQ8X~`N)%TjzNQ4Ck1Z+aWm^_i+gmiU9>1~R#TWwa&{?Wdvo`@1t zuc=&xTtU3Hw|C67Yga`MToqZ!#1k8PpC(>m?HIX%MHkC9uAf98WUn>k>7syOasR9_ z>AYa<1B<-FwO`k_AzNXSC#+3n&od&UMuCY8(hy0sq>ObfO~mAAT>RY6{Dk@WMG2Om zhh@2qmq+}LY1a!120c~872T?|zX?%sqyr@w>sJ)Q%B8;mAwJLe?ErZQ8#C1{%|oIu z?I&#~dWW5w8Tp9Npf3a>X9;M&v@y;Tu8jbq1`&iHKn&M+pPWeyA$XT#~;bVYm@ePLTHU>bhCKazo2x zb;A-Q$P?v7y{7XOk#$y|VcjYoZpbl!YoV4=GON!UbSIObkg<7Ks6-)0kfMPoS6HN6 zqf_@hc^AYXU0f}p{d;$u6Y`#RwS77*B4}{? zT3OrDf;`xNV1D;E|AqP1OJCFc(CAoyEhKCIdF^i;xgMaAm}o z-fz9aRSjzo#3&9ZU+XGj69CDDJlwo_Q==);-g3E&k`720?_w~dyyF68$kkC0y!-g@ z$Yf*yC68{f_rqT{_?Wx2I`F$CWA?lMh{9-O(}G2l@?wqH)vMR^9H4#D7SvMZBUKQb zM*+!@+2duy^{1%1bD;ci`CvWe>3$mCVct$8f-ZCg;1uu4(l;O1h3}N2c1Snz|@|wWbX+ zfm;{j5i;uf_3JvZ09;-8LEs@4%(GCQu>|tAzbA@xyQA*NY%b3=agh;)>UfD8--iLY z%h*m{;NDOJtdhU&hVOz*YQLeMDaS#vIDLM3or26X$_eX2lIa@xk zWKHS1F|Ccuf|6w+%|NYLecQ&VcgPKgx-fdAaxmzcX7zzNkv7!njnqn4T(ee2N1%vx z5X4Z(=`7H1iO&EC7Y{M(HSF4Y^jsakrE>8hfE=*WC$~y&Opl(Mv~cF zsUQV232fmMyTtyN+Ki#pQK{dSSN)AGwgz&s> zws)?YiUz>sL13}Ti;zYDGJvB*2a!rmH}ZO;CPWVZ>0`6^;I65j9Esp% zf$7lP6~e_@t^K`wDvVF$w~Z^;^gNYGNO~&$=wM&dwz+LBSqM&N!RmGGX2UCM8&c`= zeo?Ry*xc^~wKfc|Q={DCx`&_+3yEtRkF6kgpF6??e?DjT$#QVS0a7u~GBHbW4|`W! z1n*_WltEn@ms+go9FIT#xXx<&(G(?s@^++(a;;Wtk|!Icc^(+-HC(w@eh^=`Ae{Q+ zHx^n`{t!6M5#0`D1LB3%jI>7Oo${hiCiQc=03mV*(q0gfJQs2q1wv`#Vz>x+e-NY@ zWrx{n2o`^{4hB$$wC#swshS7s6@}y?L4CS%o7A0tOo95xiq&a1v>x5H?<6?oZyBH; zyCV!tTHzLDJrV3R_Imv)GR=OWABFX$f8B9r>!|d34oASc(*JQOWS!+v{m9(Cdsp3? zJ6;>#*xWEXyE`HaPkRmEdI$*s&N76OY$wHtQrj1~vpHAR0oF06rOuZXJYyCJ?GNxq zK?cTw9k=Zy07dU_*8~A!3=Ug^6$Sblu7hsu_k*#Cw6TnUiB~*!{DDno0fO~&Kl797 zj&&BW&Z)d6W8TgUQ`y{=gceM<6`ij^uRSJnQP!Pdr*4uy-Ai?sv?X2R;2r|T_f)2= z)(%|9TJ>WkdBsY&YvDzJaX-vki$F4N%~VQA7QsCfeaUCQB{B@;CoK3w98fO)H2KTy zGB8Y)&8X|S3})9%a$rf8*1PC31gCJWfyfLdc5(Al*GSK0&sn1!bnRAP42|O=><=~X z2Zk5gd;-0{Z}l5Ts(|7T;R=qa_8F7IPr69IbN6tH6AB{M3Ctb zxP;-J2D!mXNHjSc{cfW0F;j6*>YkZzSLYA|2q|tFVS|-}>)b31PsqFY8{)y=VZc=n zrGc_1Ex5t}td3hkR-#Ckzr3Q9OX~LI_(1R5+}fGC-wy`=Sy91)umit+KW;#*t-5~w zh7v0TA%it#eErx;V$8X?Pd$fIAfRh3wnk+t-MwjE_ui zLX@_)>^2|3xposu{_xC~I~jwvx33tYJ%)*C&XtP=kx%|WG8Y5d3-aFu zzHre7;{ZK3*p?YrvHsyQxhV0SB3h>ai9mM0+A+_XJP)f@)Od`G7SH*GpZ}B+ED#JH zaTd6>PR>#l2B#Ra9t(@d*t#Sgrg}Jv2iU;@RuS(uFm(e)-{I8AL(L*6Rb#E(7UO z7gQnpSRi>n=ODC>$ZI>AgM~u4XIH{IViXctA!5ah=^iic9^}HhpCz0-1S=))h7kHe zG(1RMa+uao>v<98tc^kj@weSGM=B{pXSSQ=Ir&7z;&#SNeWOty|IW39GQ~Q={g(CG z3g$O%9>w*qTKgG{E(?*T$UVxngs{}10%wp`*_bH0WMtHq6B-K@x+$Z z35-c;eSCj>Sa{n7?vD4RQXAk`BALu5b(HoxDyT~C!)0uct@#k+CsgjCQD zSfETfp9PdZWs0TAu5C)FGC`SW#30Ex>4#AH;p&>-=(FU7w&3=dq1`~vvAU6W+A2hy zSzy;5$Y;>ag|v{bV!3FtEHd@l9cf}_ILs~7t_LhhnH;Dk>rsRaOj1x+##yedi(+2e zcE7ncPWz#~P)^i0p@LbrM*TXHO5IWJZi85GW^2Pk0tz&ne&Ze;20Pv?7RZB{|1gq8 zA7VY^(b0jl^V2hp?mdpIy6^sX&-#tBch+#XUB^2K3^7R=kFZ893k1{{c3Bg8`kBtE zkCz605jR056WKHwGBpddo6W6FUDFH>kB*hRIp~B}e=v>=d{J=i7tmI)*a7-3{d*C> zi)de!3jO7tlv%he2_h_tF9ZsiL$pYMbWoz|>Z~d9&g>X;h78}@etCHZVDE*7wW`TksWt%DG458UQ`85}boG789tOONn z1$mN(b{aNhH~!u1gC!-Rv+pQ1a)~q|?zP_m<%NRFW`DRBqRhKObc#JBjB5~LllyR) zYUzFa2KWu@5t{~buN9y?%g%y1@SCPd?jx;E5C$typD(13>s?yh`>DUlr@e;(jm{Dr z?Ckq&h;kHf>k>J6j~_O0M^1?qZU<*uOl&O)uARBRquv>WKTj6+JF}ofw!&(~Ko@47 zw+btYQh7>Af|Q^LQWta>KzUGRmPDVJ6k*E?E8KEIxKrLbYpm^XRvVtQbM9GN7v)j} zJ;72aTV~)0#6p=f@j`t;^jVidUC<7ZC5u=h9|h<-QpvCDCqdZ$%n=VLwA|t+KMu$* z+C0QHtZtq7!V{6PL9E7^n>f}PK2<(XDCA&(FC_q;1F4zvl^W@sl&L?Tn2zq(BPT2eyZsk0GeMaRI!kQkxg zOWkKG{2|ZAO_Jaqd2JW29(W&77{kKRL)Zz|&h`CPRqCH~S?@3IONpMt-;|#_Xm5mV z=e+h4&)P*_aQ6nm{R!Dk0^Jn|pH!p5BsD?o?52vHPzuA$ug z&(J>wlp}))0|#zn3@!}RC{BC=`z||^g@VQ45qBmFxU|~$+qBq_-jjSW5Yh0$6gARM z`XC8TArIz<0g&{a)mr1gyWF9#kax(}Q?S2H;I0jVbzvNonRV3&d6)-DOB=X%P4zQNyo@;8<+4$isvTIGSjH)*v9 z@jR3ntTKGJVexIdvDP!+Pq&jiNdXz;A{jSV3)Y;-DQ(OVu62XWdWeS>v3ZgQ8& zHr+o9BB6a{Qa?ZYQ-9X{`JexcD7!)sm1xNLA>&atgk<$Zu}z7>?Fa`XCC+|JIw_bS zr1px=s}rG@{iudQI0~L+B-K4KiOLiDeN=l>%Sc*>VUH}}_Q@KdSPS_|riE;mR_B&= z6j0ca=eAzu??KO`^SGmm{zyJWVRyed5b2IO%NB$vL;%+_7hTX12R{;$&s4#Z$x-XR zkr7t%mDX|b8POb~f*IY>ZGm`gY zLFzjp`D@%U!;G~SwQEQ6XiIFRVhx*3lV{}9K+3Y$Jk~x>L_vPz{SZdVi8>)lcemXP zjj-73NWR0|ELBcPoI**tL+ zL6}ZKuFsW(V6lH!2oh3xg+xKu!PvsERtRQ?oi#(UD>z+9fU=1C4G=w!XIekv)7L?0 za6@xe6v~uA0Oj2e8gfMx_p}H@LUTKpp*XPV2mX63adGwOeuF6G@>zX8X$pdKDAnBJ zU$;W~C^v+&b49yokOnI-ZMjX_CcoPAF4`2zsh>2>0s}s?d#s637f>l01PTKX`+Q-A zp{_Wn*9{?L40sS9KsgXl!zqrq$T5&!gyI$6{@maGPkWFThy;0{ebeSSu;8GCu!btE z-s6IMdA210j`R`2zON(;SaM}u;y4E(B~2WZJ7nJxcD1(2cg?1N{4R^AhJp8yF47%G zi3@^sa?l3(TqLdJoqhDuDLea&6)On(ng5y89qkHI%#3|e>JY1|BZ`kYAX(%)iev%X z#^=KY2#`DU?;vvdf-1O7JTL&ey0Y~&{~gSC1+R0KGn>SOwjE@}to*#4z}{HtJezk) zU$}^VC(oqp*cdqsR;E+0L@LcmTeqGbjFGgPxH6T#jJ$Ib60D!TD47ggx{(8?+ZL9z zyplZnGV+n@$xjqm`;CIf+E(Nt9wSW1QWjxweTFKFn@22_jD4gD4;jXC)_k*hts`GQ z|8qZMeCY^>Dld*u3T2?BgN{r&cdS_d3@-ImElxLo;xl>}v-H8a!``WO}-T+*;u z;3{Y`6_d<)#4f)f3chsEUezQ!2cmzstrQ@WEWkP}2%`gObk-9Ym2^Fo ze=Qkupw+Cag>P1rKS&Z5C$7ihqJ-2}mT4cE95!y7ky>`_?_nD$12)o~MeO2WDEU)s zj@Op32u_nM(fNg3xS(rc;9l;FYss5}n#}5aR-@rXuZ+Mi+XBUDeCqGN6oTLnt`B2j!o20({c`-9aCu+!@dj zT(}mhFw}OHlJub@+u3NoV>RG6Yw=~hnH}J`hjwpW|LmaZ>LLtW%RRLDV#z9RSUhLl z69m=3b6j0;D+j0IJce?i@8gIg)4L8cqL25jziQzGzq%zU)jz zP>WUCYZ!tSs99twmxI6~57=CsNv%me#l!TosRw&6=k0GLkqs%4-&h#m&Y;Xw86vWY z;vM4IuuLSS4McgF(B4(VN%=Bag$zY@&|m2H>~V!0iQ89^(bNrn6*-P&Gp1LuG4Ge2!U{ZoHVXY%d1V}XSylMh$u{Z7k_i$CB1RWP&)hVL;;y_?#(-hhBwP1&Gh1BG)H33cld}L)9 zc?ic82@_?#w031S!BR3gnINVO_}BfU2K+9`mV6ocZd&_hn3=3I>$Q%jNUQ_l#v!HW zfrfGSk*JaA`J`75NX5X`t-mcWZc|V09Dm2PUWP-Svx*?)dC8RFWLj2OwE`I5k z|HtK#EU9YAFl;<+(nUI`JkaM3$hJ9zVDBEC&pgjA^IhBoy}xIHy}1f2LK28n7=f%5 zNW0!rg$5-W0e9`%HJw4`Z$GFzTY;ww%MT}%>~&Z~>;gUiB2s3scxO=gLHP|nAEf}- z#a0Vft;NZUt=!pZ2BewB_iFu1|^1if_y<>=--6jXGWZJtkI*-SxXvQ4dhK)NEy#!0wmZT!od4npSJ@{ z5Po?O4v3?TBskLbg5)s>Hwi-d%mU>`+Ho_)ica2WgPf0(pUZ|?d9#_&JOlEq#TStJ zc5f^14+b|STlRYtnD9d3K_1L2$FqVq#@G=@v3*qGtXEp17gSDuuudj)t+WSM|CBA0 zTeM|l0ATJ~$di+M z{s_0oG&~~+Pw(%(T+6sa-beB*+y@(u61?Rw24A!ZYWK6p>EU)5j|kc?5=E>vmkk3pQ>hyXv#v0oY->(ihvX#_T4!RDWEHK+ z@A*Pe1v!F_`>VBDsL->nQqyKvf)C&p!9H5dzMcoZz7PSMW~aRP+XQ(-k;RP=1j!b& zVM@cdiQ2Pnhb-vDRl;eFYKH2NVz~6mp)+cN8cFH|mQS z9tI@9IVGgUvbldx1c5hLTUWu^GxcW$_2fu#*r)5Ow!b>1)Xw1gNj`1AEQ58oV;7X$ z>*BPT#Q@1dfuQ|FDb;a7`8uV9vb8hR);*DfbJRVA1L8=Vbmyo?ON?z`8_I%cb6iV1 zr(LmT&DCKTD4R+9b4M8LZ?0ba;2c{4un!yWVsgh3c0afdxv~gJ;(5&2Bf?6eh_0@z z%CoK8=A#4%CdxBq9|qfg^_%|ffYNFeSKGGTxmh3rS3wl&sK6J?0DXyPht=cT0qHF3 zLQ*z@-~#RKT#yQ$MY3GoJ7v$6E&ZQ%15sg|Bv`{Da)nL)xu5!i$j}xcKTP19HS36k zU^@&WSpMe6*g_uc_+r}*auR~YzF=&YY~A&y!L@B6okS`w$vA_|vO36g;ujrzNRLhy zNcpmM?9-qAwD}AF(f?Hn?9immiK#ZFjU=Ym+3=mUF0A8n#HkywMiF?zxv)W^+-JYr z9+MTI>S87gx8jLvpx%wrG#t85*B$j zYkkxeZ6qW@+Lv3$6$NB{qbXxZjdfuVVj+CK02R&Ji>x8_z@&Yy}r3^bevu_Ig8&N_ZZ$Pwr{D775Ew8dytAa1tzC<<0A+xN;7H z1#lUZGEjF=$v1T?%w5kzCg+hXtZ%dX-08oaN60qq6&;{AMs}u&x1+&Y}#~C+ORV? zdwUNg4Gb=`@@g?}Kt&P9>S5hV>;TMwxUq3M5C$%IT<<{HfQwSPYa1%S*12hd7Cb0ZKKuvpe+JJ1$Q{ZA%D`o`k08h|2ld53?g*J9 zYpxy%4~r!ztgn(E+858e#$+CWJ`X4la8dpJB0(A9x)-+b{zXC;l1p_sd8FwOWi3;%leKP7OWGsl zYbVYo;BTx_6={bqu!(zC98gzy9?C2P`K_eW(2n&EfK(dnkhVK8A zAP7S5M3v`JX3Y$fQg2u}BuyjH<3{a0wS0*3iI#>UDT$Oe#ab?cDWJGRxNtoTtm6tp zUfyj9kmOus3V}5MganxsBopLGNu=c0AC=F6afj_U#_o8G)j~?0d*OskUemuv^;>3; z^TNe*Z@;5~K}Zk5VcPX0sr!zpY})J3gMOzj>5m`z5-2H?^i#Jf$uCigmjNX*$a}1l zQeTATC-4X3G~|i0S9d zR5^r!=b>nlKcWxPa3Cc_9CQ)jtm25kn+$5K>9VeE%U4`%{z(6<15Ey=zEF^-_xxJb zG?E{ySorS`J8aom3xc1FyLz$Z>d1&Yit=8pwakt}QXfT-KI8%Y&MD$HPxKEzxw83< z3s**z4bO*Q$ypr$=r>`{sAR`l7T#rrmS9OkT+!v{4zZgp9BU z8f)leU_nO7bttbr1}ZOL1d4%c)0WI2H@v81l5P$6?-42Tj>3VJ8=1j1C~#yM^2f%r zc6$hEo1!Q)SeN8H*Kr;an3Xg^;<%pgTt_rT4H8tqEwrT82%@?2y&%_dE!VAyY{#Ok zL{Q#EfRJ*fEa`XceR+5kNS2emwAV#Io$ABKZ$nlcdDcmrtVO`S;UOGMPRdx?RJPOBvJPAaRMIkOE0l zUPwcxur{~fa}Vo_G^}~MA%6?$mG}7X3q}@P2-0E0B2AtdGM*%QbxUTFQnyk+lmiyi z>cL%0szlxnSeGPqI(FxSL{-)%Br2OSxzxy{O-tSS(n8Ra{hAV4>MF@(UT#&I(wa#W zg*=rXQ|qU$^Ycg)L{Eu2eL#znQa~|BkuN{OuunwzMtBWA$ znYAY?QdS+)IG8V(q!_4wg&PRMfYO1b47Vi)Knh*&^^_<-=q#&37HU=83ZrMRxYt2v zK)k=*^CIN<7<8!MyTj6hk|a-)va)|Wxby?oxiC|};Ky?}x3+Z;5~rkI6i5U4M44jk z3GPt32bPz2exC)G-+5)_fLj;qT<%NTad|@#+}+(#DHx{QckiD5O`agA^k-b^;wqiI*OkG%a&~|Ov-Sysw=C=IvVqc0I$2~+dybp8 zIH1hbVlHKkJ1cwfMFnJ8v~9)#W9Yn@_At<=x3;!)!{8_d@vLC8S;*MkyV7Swp$SW0 z@VqKM0?xVLW~IxHSLrbkYqUBt4%k5wvR6H+s*<{lys@?sTvqTOKl;*l{r!ru#HA+= zgyYIMD+=WW%k3~QQ?2D6^6r7RJq!~dM9w6rP!y`cH194s8 zt$uX69mi>>4qICrYR!20TQ94Q0`X#!BMuk`84r=Mj$DOBDV>K;|MZWUPygum8B=&l zj>o*S9&pjCeNFPxH^PQAL2BdjSx4x$1rB>JMGVA+CfWrPT3$}$KFarCSY zV+g4kb&t(R6z36&eX<~my%4v=bh>zN>*RqFbKcvk?Mq#!$N9%~E-5-DmbV~+oBaki z4^FWeZ|R0~xXe#o~y2mY?ci9()N-!f9MbYJImXQIB3a&>?DQk3|YH#{&CAf z8W_*yy|@N=1$DVS5{Yb(Dz$I!r|_>&*0sDW!s<5a7-*( ze4>Dn6Zbp^+62$CN}qd|`+Fz?9appTco58FT+}~xw)Z(3Ij@TXZu-Ya26$& z&cZ+sNpVUPGycp*p}dh_>X1#Q-E&DB{e*j$4V%9OG9Z9JkY=}b0aBr}pptF^0>m$m z$%X!Mrd*@?LAio^oZaa6QMF{@0*mVnd7w>2_vyuetl8YOKKzi16H$C{TMGkaco7p6 zww`>I6%$Mz(60%f9R(&AXj{zwFS0HMH$~)!wN6B(EqI>4;}I-itxI?ju*r7T9PICp zNfmX$?;6=rcml<-pP}dW7Vu-RYY~t)}j01dB+?bFjW)$tbRb6QAJzR?Z#L!3FmK z>;pE+R`i|ZiG2E@dv;;zBmFa=x=5FGVAIzH!>otxy%iS(kT`oZ!P8K;$q~U;r2SJb zBWcgw6O$ZBTVTx^)+nikDVM(NnhY*iXS(z`Ym`Q9?8|u2Ih6Jwq`GxrhPatF?$}MP zdqTLe9?FL9_rp@SX8Ma>9>FPm;Qm)Klg>npEHxJgb4L4 z5e9q9R}$0*K;E5Y-z#Xn^F5aKMhJNqXd?>6Y{?vW!izjqR?4DOm6RYbqglCDl(O!d z`)}-22Zd`G}oet1lRWf1~vu+yAW5aOyNG41=fL~NJPyvTEQA# zC{gt^4-B*@;L=9$~76la7nXkcfu!I76Ey-+X=9q zhzNoQkxKZ*5iDere$&4f0dfgNEhmoC&0Y!fvASDnV_W4>oZLR!oavfTgA(O_DTKGZy{Kug*?R}TC@Wujd2~sEf(*N zxcKy^KVv@qbAMhkFFI0B$j2EfEnVhs&}5-nT_Jr_Ah z1^Y^|j)rq6^}3<2^e5fmSguv~WO+C1Hg(B`ZcfXdSOrmvABYN=QsuVa79GyBgaL>g z@8iJg0D;3Tj(`l~ARD3#Tf*hmakb~JjDQfbi0%X`-(F#B;}36#|FK!~5@lhFL8C+UMg3E@e4 zq*g6XkZauaq|TB;a%_MJ>(WQ`!6}zM$XE=Qz7Ygl-Zz3|2{{}{drBmPODKsF!VDP~ z5=A?4BuWF1K%^jExvKrLZgmMwu~83E|mUr{J<&-b^$4&-EG7%B_Rk- z6`HQl{Do()k5YUQ`UN3VFmAp(AY7TfstUovC!G5iB>`N1ma$P^BSI{SU`=2ZXS)uE zy0>e27~F$9=ezd7pytnu`#$;Gy?^o<2VA?oy{#k zfe&GfvbM9ctAnGT@L?srvU62CTNp?phblPqsWb;6lv5(BWN^vYGUNA@4DvKBLiW-g zbr8`1{AD^R{nX?_$zn?gxI!J(fKk_I2kmvU@s9U+XUgT<1lR_VX~r>_qvlHHM2k+P=~BlBIpUO-dLv=1+tQ2 zWI~p}F1UWj>!*x^c8&8q@ci}l4XsDN<%#y{x5n|CCGxXD^0=0~2R%HZpakpFpSFKw zH&tFGdbgvAmLQS4BKla}mRHTqDG!KG~QT1=l@02>U#&W1>8>7FEXNQ&uh|T$6QNkcSab?SkvL zP~IV2-a0M~$Piq|mCl<&X~pD=Y@ZR{|2VIk>WZvr)ofH*LY+Hk=ypdY+xzNXPWp)0 z=wHX>;NP=r9+YJHtX2R*oAz1JD64w)SnG*0BkEyah$^zN)2xl7^06Nmq0J$pX~}m= z2y8YlKv*4t(tM2xl#om;RK{EX>sg^vS`Kurag(6YAeUdfY3eu0vx zA@wR_h)1GEEg|5o`%0ut`@mW}J!_OyppdAX5U3!D5@HxeqOh<+q?Du!krLe}@R}@X117&Zdta($gQ$bjeWdbdzY{mX)F5l_-MKv; z2zPgPRS2m-leBLh`)7F6?4+Y^;Y4UU_Jqweiz^43_^a`{KR_?8~v`YZ>a(jwzdZMB4&fh)4XT; z^BO13q;q?FOO)W8D0X|&>7%WFQF(G@71HiwgMn{N2F06iy)Nmt4`M*u+}JX=Z{O0t z0c4ZG6GeOPaF01TB}$SAkr5~klJDNgbcDDuIl%z#F9(0S<2%oTWTnW1{FE|j8F}n) zX$Fe4D1Xd$FkboQ9q6mH$t#;zO&KIegCF#9g}g%tDaz6e^*R+89FlK0 zL+dYRfBWoo_bmowB7ZMZFwmQb<*6wPdNUwC4lu;l)GDx zS8L$ja(ewF@R!*|fu}`C)~6bado6$AO~n4g#!Cy*|B0EU;3ipv#C4e zjPf8rUP5IKO1G_NB8v^_#NK(*0104XW)Vcy4QX+0Ee;6Z{wP4#XE7EIq}=4w-)`CR zpd|#@AHg*aXcG|Oo|G+Zjkd8UXzO?a(CkjCHPcw)evnMs^`!mJdZxdgX~(peIEcF@ z>(y7k?S1p1CFH90x{w)Ba(f3d$UP<<=_&8W2}ZSV2-!F>)%LnnU~=Yq9;{hy(b-lv z?fN4EK%kgL!L3zN>&{RJ&FXV9wzbUg_!YYzOUM$kbY5GBFc3V&f1S$Uz9`O8rx1@Y zH@8#kxF7&&+%W@bj=7F23{Ed~VBJ|_{ds){`+yD8C3#-nbZQSIjY2F1*3u;RNxE_G z^9RkCg~stclPgq24l#itvPTFd&&jV&)`R)cs9U2$n)FG&(zsLeRqh??8mQCjplAcS zR!NBA*aZ_3g|Z@b9cMk1gIgCR>_*CyD7bRJx_AoVCha1Jc^Bk~=el)KdVWUP>lh#e z5)z5U)Inqa$aA_vT=S*rwS{p(oBg-{_)pGUleI9(N)r_bVd#ZheUwlUW_$)tn?*hS~ra@rKu3Z~!cnRcUV_V1ovSW9T|Fle5oP5H`h3@Ii#tb*wW3P3JL z6d~60q|6cFn;BFB*Nue%5e%n)nhEYZi^8Ugbc`3F7%?c^zyH8HGYu3ETx+ZwVZ3y> zatbR)2i)7Xx36fqLJm$uDVhbYbMOmRFz;P)z(B`f?;reFXpXf4(AMP5^6rENSLuo% zYZFVR{3>2M^AATIyDgWCI&RLI zT#EwMO(s4l6T9YVF`@<-`5tXIDD zk4>WTe(x_II;>B5WkR&Vf?)_8(K>3a=@=6X&VZcji1LYLl0B3h{KO#)W0KW95M8ru z62+@l$?n$8AiFvDeusABaIVtK+9W1MfD6>&2Sm>dMApeCihYu`J~<&bo|R<`2$z(_ zkacMCyn&Ddld?o<92?q%5#94jcE(AQw&y{eXlkAsxyX=D84Bw@i_}Snnfb zWUqB%GKFc&ZCteK$L5FrlMdIfGnO^h1;vtQLzzHR7GKNCz%Oj_4AiJ#pQIf?3(5z@~&I)z!uH7fb5g>;!A{2(l z9(znJ3=i(#o14BU0!Lku9$emPwMM8#0A+S#eNC-Z>Y6L%&-I`$1L3~Z^P>RDlCq~Q zw>aodt~lxZJU}>ntRRezdYeXlMd1b!J98oCk;>b$omn{~3N#O`D$=K{B=_>-T<1 zGAAUU@=~(=G|C`SgRFg<e?17YQ&(0%sveps)@>D3w5A z5yD-aNPJTFxJAkja!%5*Ks3H7C?S>dgQ!8EAXM@`-H*&q*dRPUD^yaBp>(pK70ORO z!Epa8WAf=hTTn>;Qdwy;(r)6C@E~;kAnU*aZPE|gh*|_CpXl*SAdDb$fi7uSO1uk2aZpI3SFjcKPq8K6A+gF6HP$zmp-*?X6kdEmMz zf`Ot-2x%+?%r z9Sx*|dTShM!%P%7GNy#8m^E1ujBNU?d)x$25Mf-BL7Xnt zp5fB#|2s(DaN~kNF~DJYjEaH?>A=FyFp`vj+B)zzy zhPf7!KV;I|fn>4yG4CWydl)JAw-o?6BKjjl`aFP?(-x4gC)!UJw~Qf2k-;5FfA#nv zvb{$Ea>M~ibJnT2@LQLW?YKflAuh@K;&1+%sT68b=A!h55iQmz)xU0riHb>%L?%G8 zAX@=AqIFoWm`uK;E_!}~cWyoS$8Nm*2<)2xEli1hor7G zn1pXki37-4DpQjBwdD;N3~rX}CtMH+wQgZ`%T8O)NW1CR%&0B;Q&&DE-NTfSEhRDb znW>1-?X#fegI=KIY2v=8zxiD1CIm{#YZ>l)egfq}n=65mzU;j-4uOh`AkiLp)IQ!HC|!hURPX!=ir5mo#l{ZK}(jYsad<>a3M~ zmvhuTo-znu2Y%9TY7hTuXC+j|F zy8uO%GGmV@5Y?8EA=}$KRtO7Fa>EFg%g<%ESu3{@@*y2N1YApBKR#yGe7dbV>|Wh9 z`+EoD-+2e^gHOBQe?o{jXFpv#nxunuUmF`+kz~i+3;gZR5n_~XESmJZQ-2}#Y z!6boQV>LRn`ygpq@g^GGW-Ne^k;dYl0(Q7-P6B6l%5dcgQbnAd$WMPgq4tB5`alBUBfDZ89fBj z;w+E2{IQRhZ3lVlGU~JP#kFR}0Z}1ajhe2_QZgm!N)prq^5jUA{1_zKK`eEjtW+*s zKt4>Ag!v!r?dhy0X+@Z#EHMx}pt#~HIOt49+xl^Hsvv?Ok{*MbA9f&!zK83XY3g)Z z`hFXpHLc-gTTQ!Yb20EPtF(u`+bVMBuM>T=4{YX1$CwwPi8L`oX9s{uI%ky=zXj9G z0H%wTML?qTTCxIc{sGVCyCn>~OHW@KAR(-=BP=wQi~+Mq@(%<_7ofwfR@O|TO2|(U z;9WW76B46`nwxEB)Wah;kP`oSR|Mnhd$1aqUxcOHD^1>|2~clg z?Ze-0TwPxzs7o8^)2@%8uQKCJ5{Oh5l?JSooM+HJI>H(ruCUqd61+by5w;v%IZq$* zfBCe1#~oPOQ7o7Q3Il`%ML*0c3&0hA8dQ2BQLR5QWgW<~eAal?BGOI{eAp}y-8mwXdeq2hlazfk(Hgx+W`W|8FMK0$zr=stTLzw&a zf^uQZr${pHdm5@-WV(yON94WMzH#BqH4>k$CzETXKM*V6^xjaDhLwO;$S zU7`mKIxX+kVk9dojjL5}>pCp`Ta*Dpeh2q+%C1O3d5Wb2ZdYx&HwsQMWx$>wJpYsv zv*Nf(S?dqN*ItWYWSt5Pk{L)zh7Kd{GY;(FBk8e!j=}kmIz1aCF|=#DCTdgn_z7z; zevog3y_;ooWj1x6FOxi9^Wpm$D-6ml3KW4sPUq6Xo%jEa!VrG-#Q?<`K|npS&Ldbp zln19ckHDedK>C-VC0MB$4u3c)XBJPA$M{l_+5mV5-=fO ztidENk~x@z=357Qp3k%7x0Kv7c1@J`X}h(5jlsF0mnMFt zt+*s!1c>&@WQ-$O2uRKqizTUFHU{mUO5@Iz9hEWsetfuZdLiVRbW8D&b(;t8{AGc6 zQ$Oh5l%M|q!E-2$);bbSdR+VUyG{7P`{;-AZTyZX(B=wC5D1w+zfV9WkmvL@EETsywWzXUfa5o%Dbe4wOGN496wyU zl~!U;2pAX6vnDHuwv?Sn7L}c}58Cu1?Ze8prjkF#`gvq+V@ni7Ddc_ORcQD&B-{_ifBO#Ja zM(xBZ`BE3WlkFw)C2MhH%tBet7t3;QL9YvYKY#!R!<`2~0FL0>wMz81Dxa+*mgd&fIOTtLaTjTlNT)3u_() z`T%96;m(Cn<;vSyAAt91N2U);eIb`LG{2NP8(0I|$EQJnk4%0=+A?9$aVu^q5oH5n zli!d!m$b8iFzq8#G~K2U=8*9AfOdkFjJDC0G-CblwX3Gps85BX$2~qN&&tUdLmRP^ z_h-XAl6CecKV+n7nEjOcIg>Fe5BDgy9>syZh>9C|DfDS!T3nfLT19c4=N5o90e2g4 zczhVDj6gyV?9#-fv(XUbn^zZ*E&pd2P}oA+rk-CKVSL8ofm(jX4Q>TBu(=LZBzEl}j!BTKB)()CD3FUoyiHlw4L z%%J+Z88q+fS}&}2sI>#Bm!Xhh!rb?ZkK1>0_^eR`4vDiqiu|xg7x^HNgnn`~jvo(34oT{7A9A#Xx`ew=oFtEa0W!zkcZLO@(VD>)L=uca(R7zHjf@AZP>4M$@#r zBWXW`>aR$X+A%9zS2Z%@=FSbXwzX?k*0)Sabg1)!vYCeqRAVj<`2Oal0$4u*b7gRc z=-@jWoXyFVgh@9-h>|qUnS+ssx-j4<0)(QApEgPegC4$}D2Bo7QG!SELWH_gM67Zm zUnCuT&LUkglDQm68knJrqbw=BG%pGU&z;?CNLKg>19)ITo!j1I<_{#|Q z5zhN(6&={)3t|*Uv786WYh#nmnX_tvL18#+9h?cg%R7{O4Vc9n$$)AtT_{E!&+mE) z9^1&4GO7@U!QbSlSKXkQO*$?R!Fhzf^i#Gg81%a;PYOjlV3&%x5U>>kd+*%SeQttq zZP5`dhs!2fDpO*DzRpa1)IC`CAUE@#=_hsbWCyO)w3T`Nfyj*vytrgG7qn)e{QT%dbn?G?8o39k#ZvC~;X75H$`w zKUn|gCu=;94UO%_+sg;uA8s6Su8`*eifE%-@QBt|rBMYYJW+N%qD5c>h!$(HPJuOB z*=@5?I#O3G2uvKHw<-D!lgQgWpjan_I~`zi+$p8q2fXZa1<}JqX-0|86O}Ros$6eB zX7gLC>N>EFyqoP5=B@L=3YITVuZ3e$k$?*hx}GS5g>$H+1O;4)iH~p*#DfioSz`ns zS3v@$p)l34Hr zx@Q=HLj(jEVGSD~$O>f~hN9?V)br}bmMO2SX?tLto42m&tO({e3~$TPD4#Bv<3o1O zwu~0@J#IcVZ$B&tD!s5JBrC!fyu2}=$>Kg$J+|(MzztBiu^{MKGrsb~DN+Sps+#*P@9v0>i9l#~ITxMoFqE6$^l*fDL#C_Gx#)knnK?ZXy zHtfrF7Lc5%f%u&YXW{Y_t~D8Nu&N+;9bt)6SC<8s6kA`Tv0Menu*zz@jaL^1WyShm z-e)5aE-owSJ6ID*CuEH>rd{(%)Y6Mw56D=A`9VL1i`+c#G@w)Jw&Vi^>CuK z`U(B25$gIv9-@d=?y6ZY97_Isv4|Fy4E@9vE^Q~a*npM|etV*fCq=;z1J4geGMop- zXB6+1&8zaRf>~YL)kvPYzmf+>4E*RqpnQrtYpwhQ1K>V~+~Rtjp%tHRDQ5IFPlk|P9o=lIHbXW zeA?g@6Io8s5w#Y21gc(_cH&u-QU%d{D?6rdL=Td(Pv=)nro5>xg8ueMWJs)+y4P6O zn?Rr}rDRj<`md0Ul%gJ;YFV~vhC@^~e0fwupsD0Kco zaUNLDvbqMzBsBbywZs5<1okAVtLRPcymR)1QelPoKahZoG|KN+j=H|BMxzlc|TcK8BM8yqy0~uf-RFKhx z$)r?v}vIoanJ_35w5If1+SoeaX_qiC+}HbpSDKD z1?7xP29PzkCOq!?sPljsdc0}Cto{^+`tiOh!G54@L$rD$Av(1$OXdP=$&wK!gYt$r zJIfUC>&M0Q8)_|tP(h&9uiP-@)lDU~Y=o?Bmc8es?46YfvQ;}d8WVD!=SUQ70vAe< z%t%}J>Lki#kUR}LXB#)dpk*TEt}c|Gb<&S~#jY8gg|`*SN=idbk1{IM5W)z^&CM;- zY_Kpp&^RP#rJce|lBJ9CW4I&uITj>}f>#$n28Zdsr{`4O$-!^IeuwbI1%}yLtQ&4% znG4^0B-!)H#~Hz5!A0C0Sp4f|qGo|Tu+}#=Re;{RcTX4FPr>DnPdS`SbNc#4=6O+| zeENYMUBcRc4lN-MXbj3(LvC`~TQYdMYb&%7cliJIqt$84SbO#~QUAD1(w1?b zyn6Mjy6h9|1#gnUFsW;S@7%eqva48}@7WRtCM=xAm&pmYZuc^Pj2ee@50u|56mY8( z8R)m*fK)PT*qW@D*fwg>3L#oHBuK94E&|y4I=FW5=wFrU??I@C^WzC5Izaae%ug zWUTL{pEl{po^Iyc^;`zCYbNQf8Jy)Zh%OcfjzBqUpeuI=>H@;Xd(?$e*P%^&V8n4l zoX;>GAr#Xkb-SZ77{DPZ?3f?s(vmG?tn1Pol-FhlCCJDCR4H2%wjWj@^#1UGeyGIC zJFlN1NnkeD?f-#*OozBng@WhFfiCMv$g)AX5_x42%v%G2`^4J%mI~H=slQl(8oa!Y z^Xugqy~!O91CT@KrU6h4{O*A$GepT_Mia}OQ#84bd_$ISL7PRiAfzbt`FznM;8AlKZk&6c5PEk;T(G4M52~m(VTyy|Rq@*&nf6AvLRzZT5%!XuYNzi1?Afaxy z`%ubW_jMv_mDI=fmei{ZBCh?-BUpOf1@+ZgpxL=22xKN!tXWad=(GNP2BTo#VTq^% zUpb=4740(2Wf}#QU;^Mg>sg3YsRQWS$-H<;a275CGElz>QL0x86(wAS471^B>9Z57 z(nTQdvvbl$Aw+S3wI6qU*7NP`Tu~S9l?oG?c9IL%KJx7*tdP~L_oJQg8*P$j%mdf3 zE~>J!rt7gFaP*h`{Ri5oSh~RbFO%e>M`)0)unrdjl=VV+RTW1El*xX-t$ST{`{@bM z>IC)_U4&>Ao>HQf%MMkU=cpY$Fm)lfDC@pWM3GJv)p|28;-7;OF3&}e zPn7gA0UPwkN_3RP3qTG~`1QLe&ziQh5RvXkNX7hi$Ua|?G@^j|q2wsSamv+itOzzF|ZC&tj^l4M1uEbUFo zqd!WkbrI;AGomPhEEZ2o#BdpO`)&m>D~O3}2as^*PU)~Wt<-@27l|+#r%Ff2{S0;}(YPvL z*;)v^C!RcA2-=_)uz<|l^t7OxIIf$1&-$;QOYxWd`Uw}u*f(f7{d_L8QwA6fp|m)=vecv zSvG=IP8&eka|geZcj_yOgrG!1oM%~*c#eCQBUrAzx${xrxgnmHA}h-JUiKJ_sVhc; ze)=wE!&$f-;bOKH!c`H))LFP7T&HFe&w)kimBs%-uK|Gf32l_MX4~5$V^>$TZL`@m zeU+$tkSIJRI9S(5UT3vu zPM3+A%R;B?MMIub$U*BBP=eL2D=7hlx9$$|mS_=8uys?YrOnGKXBv?OAHnnpqmqW!95hB%>N&r)%7Jw0;aQ!?^TGimVt9qj0^!}W#SMAyy2<5B8uia_1=F_S zxeP*3H43GmA3?H+L>XnhghXju$YVGmXkKz$hO8emo>dW`pRr|+ACRtKM9F!0Tac`A zxc2?i;oAK(s1E5|N?5TJMnuE(g9@4DGRSYd!i_UX zP$I*W4&)w(e}*1e><)*(9K-Ez_OF-!MxkM|wp@b}*?3tDH zEt5`I#kJGz>tVVlCRy?QAj~+fbh>V;Q&V>^NC){ush%QTURZEG z8%MX5fSm`(wqR)Up>O?Af;B~1Th3T$2<|Zp>bgfANi!q;Zk-t4ap@z_gc5^v!6fr* z>hj2eTVK1?P;!Mu28)!lRy`_U42E3c`O`eQY3h65HjTr#)Mbx4bAX7Bq-`m(Ws@9Q zl9TdT0sX+j1wjwP*bPxW>(wJIAIb^q9WINI%P9m54z!=BXGh_Tn$P7SDJ(e{uAOyQ z6jsz=oGE3cv&1imLM2)VVle}Z3mnd+iA9%wK(OYG9q#v_4uBikz6fs=AY2a~W=GI` z(WB1^W&|0WnvL4nHS!`rnKGWZG36Y3KrEmj_-EH>k0=6RQF#!!?7dVf70ik+N)|+o z*-mCox3BCfS%Gk+GXR1I zY)U;KSfko3eU-h*8uhA>73yuvWPL6-V4TfBom>_ysp50`04RJYp(v$MpiY^g_XEAh z5hAq?bo%^QM?r8ZibV|+ZWuwkyJr-zp zSZMKNv+MJj;q(&*TC@V3k05cccp-C)~l>atN4Ox(GHQXW_SMC`2h$%#LC}dfYu1Fj6yxEE?V{4rs=sw9@ zfGl}%LFtfbD;qoN3c0#cOnMLYqLn! zv^9Xif^$Si^CNCs<2of|fx(7zgoy5RyJly1S31PSS|K7Kf_4#$6M}gPk}n7r>ye1~ zh_&&ZgrLRBS)w8pIyucCT`rY$;k6^sTo2K@fA5a2(V|U5iclV=%0J?FXpwL^Msxu0JJ;y3pdz;zM!G+WAGmi?KlHEc`(QC~PdPxel0Kpr2hRPl-<|gA;2jwkpkmOR>;u+0HbXC5y#oq;P@jV2 z;?BoWTm*;xj!t%l5gC8jeu${9#gev4^hqfI zbulGx&JqbIyG{r9ej-wa8!Fs-EjtNk->U&rgj7?1ehV7td3}Lv-SzFJy`FVl!Ry2D zNRzBZ!nHeON*Sq(#h9jZ86?`EJNTO;NU{J52(AMTb0%(20pS})v;Zzi?7fAXU9t2K z+y|xcF$=c{#$sIZ0|}-90(FaJq~9Ny#~**f&NvFV zm#BRAglM7D@{F_0o`nH-!JxoiCQ33ulU~-^#l_g7<%1G0IY#}FjL8E%2Hn0Hbb6-O z==vJha)B$R-(?MJB7^Xe)YCl6oCD8bW}S$fWl^FuCIlPK5hZ8b=jOlOoe)mqaWitKfp+q$((gI+`0 zUced@0=1m@NEd5uAT(GhcwVSms9u? zSg+hZVBNYId7JJ!kdAgEJjQ?@{xwt&9mv`R*6@$vyKm$p%#O^kKGg5JiFMNjz(@$` zP6^2Z=v0ty+ClAT&-6rwq>EnE!w~p`Vhp5i=mQ!s1q&M1&YqBMsZ)(kDfOH6`UjRn zEWV()az(EPSWD;OktJEU31&t)Rbs3nn-GGvWg7K{Rn|g4TP~~@q>(Q!ly{%nF;9N* zF&O|h&Dy&4$>xyiJyl9pft?60oye1GSPK>x+#X4^T0*pr368L8*48F7dY1uwRdrC4 zbg`ytb9=|Eu35LRAW#qyfGHuRD5)R%h~2D^J%xhN<}M4%m z@}e9*@S%@u8Tx^ef>jFz1BJtZ$X9o6-_-XKmCC_IAUtcH7)UMxOgy-OBWz*%EYNp} zJm?Po<`@dmnmVKjUZie?qR#YYlM|&F7sJ)ks@Y!KHf!>mMpYGgLA|uwHV_hI z3TqF+BM33iGo4IuaC<>rHb@iwdl{~NkWaE6wrRc60PoDxLY6u%Cl&VH#QDNCv@EyuVg`|Umka*T_4P-Fvc6z2c5LvpqZIby-bKFh|>GC$gm9|(t z#-g;C9EV(`b2dCW<-(dSJ{d!M&8h}UuzN5y)#8!LmQ*>%lG1M+sO4qUYw9z`OKaa41}%u&M5Uj3u)OsFF7!z?5}TNUpz*0QE zz^oLrddx~ewqW;*TA0p)oOE(J%`?wEZytZ@S#@(*-`qBvTifPDgqNGS0fez_3n64i zp9qOJ9)HTb@$HvI5MO*c9j033lA32Koi@+C?*+5>fY9Bi*EJ=QDakWy6|74epUkMB zTtEN8kD0yu_vE;%oAU;tJW>RX+zX4B#g*!G%57NvJ8?CnvoLU*_jLe z3JG#ixEE3@+umcBe2 zQb9ijDoI@r)b$E?!?5iq&I8&GGAan_tkdpjUyTB5%xH_jHshX0dHF#b^~7H+ zP$sOmfyDXc2&A*s6s5u=y$)v$w7^(F`# zpCr=6?n*joDmTt$;}(k*TcY zQ}kNXlXS_mMk%Yn_uCDV$(8JNa`1fQA!Pwsb8EUDH9$I3nS!*9#pSHZ2`L-y$6CZB zAmamh&6n0qwy+}4_vJg0HYu#4`34K8mj7O|gnn?R~Z8-?a7kvtM zQQCD7TAy-IKh%Q*-_wEi(eHP)Km6Ta{;bHfn)YYM2mClQ$-*5VxC{2OK)Ci+y=|%| z)?CO;Gghpl!)P}_(ptVz6oPwpp@sD_znU}MdROx1o%Gz4c(;d}4^!M`2i zo{#z+xbOaJ{O5wd0q zea$o@HgY92I2#ND<^r%(Qpa>q4ipjwVg_H75e=)nB7!=R5+RSzc)UKx;KRNz7eRoS zg=K~hWdWuA?MSk4i?jWzpaN7wbCQ zvRP3E(;3anySQA?c8~{7e$mz)nL10GSGig)tNZzRK{`ke2kDD@kPq4uio46(Sr6WA zmp=@3>_Fz=xzg=Uu5slQr6>jg!D73KgTq60%S?JCi;l}Pkk!1Gehkt2@gMs$V})@z z>?v{ZlY?HXDvJM!oXe|a#}O+kVPXrE3PMUGFMZL&viX7% zm#VxjA&Ol?NP4f^61@)nP|C%xzzcd(ewkc2$wHCKl3H=8e>I{zrZk}Dy&S<$!K(l3L`@(kd5 z3HfcF+_zRhA+Q4}OV-3G@sy)e-#3NTYohN+UHP7!81)J>;XhgXAv}K@yfIa)(_nWKN9y7agpghtUws)?pwehjXpE6HA z{hXu|fAnMX)HCljJGo=Z|{ItvmKl+{Ki6@`YXYAd-qm2~T#1DMz^_A@4Tf0;&bo) zfO+OU&zmdq99*=j$4BPLXWlcF4$@gWIn)7&_kKvy!};B7H9WQ*~Bl;e` zH|td$7(evUPnh-1E%V;zKco`@PdxpsD%eB}MBxr@s7gSPN~NrIXE(UTrE(<7Eg`cR zU?+9zIxA(40t#VbhMqRgKs*nW!9|fQh+?N>7ojpMbP?9Fiw1ef=R~=XdUj#imxYGZ zJ2F#e;HB=EwLdR`iwi4b!b#RF(m}t+q7-J$a~35xCY%*0gWOJgcA#(ObGaEx^y$3J z1M4_YU{Q|W#oHfDgfQ_m&+0V`j#z~$LG!@)XRS}ArCFYl%Zvk9kMTI*9Zp7&MpX{w z(^;jW00DJ7O>O#*r%WEg9GIAxe_BG-WF7 zN|!WHX0#Q`R&-ok^dPTl*^~2*)KN#Hwv=OTU7jK2wkc#aBl@uP8)vE1KoycU)>c8T zAVT_^JX@bfunYoYgw(Yk^t~Vv4}xoW_TT&EU!6&^61%(CLZyxyP@M^HM-=GHiZtU~ zT9{);tv5;ejYX}5zKrXiI19|ym(nG3`xeTrbrZCqt<#aw1VLdnZ!)V%({C(Ck;$)# z*m%9FYo`1_Az0m5F}-5Pv=6!_znK*xC1ufMZ>iJy&#h!l`><y7yPuDs{&otx%;FMP=C zKe%W1dKAC+f9PZ8)o;CMj*kvC9ghoP``zFC zXXg52Pn*rHE9P4-enkjxPV({*^VVyxh_GETn^$(sH^2U6DZifF|8eu+-W|!~o95%+ z^}QPD@2%Hg)qPUP%a^|JIg=G3{oH$BFyDOft2!WE*}b78>?^+7c8Xe!YID)V@r z@0q-Br6PUlwhU-X2K#x@{&YdIw0j8Sg+cO$d_kB&Fy%waLp~A02u=pqkr0{;n>X7X z^Wy-EVix@8h(o__FSx-NP;2#`ByF=1mQpusN9SOoc z6VMjJ29hAcE+OCn`(XLYn=LCCJdX|z^zX3e$9UZo*f=(u&1>LB(n?29LgB?P3zuy zyssg?9g!oL1Kvq#Z!Hqn5^<0a;8AjwYp^I3Gj_`d!nBXcl>=^G^{<=eV9gx0%jViz zOI{|jS{^ydY>Cn=@N!Q_NI20_F^H)(|a zR4NFOWKvxSgId5v`DQJS1M7Ei**gokH@~_oX{#Fvr0Am{B1CSX05 zTl?e3P{_bp-~Fh701^;0mKRVTSh8?WCD88Q*ceGCBF&RC+*ylIkK zAJ*RnM=zRTc3tW_H(p~Hr@mP07F>%J1e4y@OUVMVMp{TC+X|$ULe{-?qkrt*`x()J zZmXQZO`7q`KZeEANIYb68PjXX%e*y5SZ;dlo)WBdW}!!gf^h9ezHsdb5PbM&5v*(_ zV{+@+r4cNE+h0aHpC2ryTN8qj*czJJ{krM2L`dy5Otz3SDLYwl3Y}_K(w8y?A#G_9 zdT|KWXspoqG#eiX@y^~$R44=#;^6J|JYmS!)P}!8$nxJocTq z_2z5ltv9}{_d+!92~lD{ky^E?B-{aO7c=}ECo(8Bn{3Ey|E|ezJR9rJw}i~#h6up| zJi`fiW}qHCxU26b9i)@LyS}^gS!Dj zH=i$>d$-?Ga#XKXH7|hut+@X9@IYPT9^AX7X?5j(_s%U1E$d(F5}a^Akz8A?=uD&T zzZ7T(DB#p-PSQ$yXQ1?h4okWiD37IFAx>#27oyo=1qg%gE3&*UF$MM6FII%uKz?S= z_QVDCNqs>EC@+W{$Hw}Gu4yV2Wzfvww@tqsNgFs(GSA}q{XG`X-`DcVrc=7G+|^M~ z*#PPyD5rbU&o6{v0jXo!7r;UkmX6CpT1NO;UI2iNfSb+vR!_Gt)+|JkBI+&}dKtOe zQ;V)eoafI2?UOipRw}oim0?RF}Jz3p<}2UPjDw^Jv8HJa4835%|#v5 zzop-;@7$OWEH9V?8xs2oR<5+EpHcmZYuW6!*|RQySghP0P-CzeapfbXm^wD=`6F{M z$eTv$S<_2DsYQMj_3}ebF@0rj%?7ToDeF;8YT)`oq;K?Zkd*(t0p9j02%~dARtIj#!=v+ z7cK9KqqOK3hyt(bH% z!u8dMg+~5%9TWHI1_9CPb%kYmzOiRPXt;J!B4>qb&z3T#xLGjW+HxD?u*ssltdH=rIWWT$fYZb5dxZ^$^DIT9 z+0<~;xICf6A#|4k%;x|r)$N;am@j?dcg#1w`bFIf=<4;yq+VPmSs>p~qI0l!UkK!E zB`y{uzmGrptOng+reo||?uT#e2%Gr;>iV?4X|COPLJwT${G0nmAxt^sS$?Y&B$v+V zIx288W2Ydex3{;II2|5Yk{JnkHwr%l6+3Ax(Uqh|+QU_AQkg zxLYt00a?>%UXmu#irXa<1rP2=M7>%P=o=6%%8WH&4-2&6C}j2eLFzImYF91U!o4po zM3OM)dG#gFx;Dl(EDbpSmOh=AIwrp+4XbpzNC_$5KJJ?0j#S`E zcn5R33lq5<{5@|{7s3nCLh)z40}A!I;HBZcFMPt59qGV*G6)pP!~F;5 z>tFqS^W`so&b;*XFRF!Y9&il=e$|dOXapUB5P*W@mw|8YER#HBYjaEex2v_rY=Y$j z;lOblB%&4T%%WUH=qD&2kPyhng9rEJod_q^6!z3I2MDskfJ}&TBHZC>T9IRgh<9?( zZU}flv_l9Xq{JOWq{CXay|tykJ@(jR=KA&PN?4SD2_W5Aj`*EtT)TEn57yLSjojUx z9D4rB6+MsZ*qXc>0!Th;ERq-E!*>pp2kKF0$K)Wtag-%z_2b&3qa$mttZ8i!J`>+WHKkPHDD<9X+I>O-U zkGgSS@`H21fEQfr|6CL{V&}!z?Yp=fu;ICrH~%^zYVr$#II% zIb>I+NY`lXdyMYQB|2)R|Ax6%x@9V(S53MnV`smvlQTzE8SmBFCF5=?Z+gR|lCDH? z+tl0rDJxgdD(}az*U@rKkv1VA4hgvj_qK-(9aohsov1Ym!ldWbov)$i&?g9U&UeDp zb1mcw*E#kggGk|;$9Lpy*qXL55FVW}Oc&PFQWXRf*#2?Ck5Rfj-Y-k*R}^s4O!UEI zreJb~irL#g5Z&?692_;wQN3@vgM>Mju9CC!-r44rCR>?bmkR~HkQU?p z8V*d<%yeS8Sww*ehHFRh%Vl%wGaCl>qguTtg63qtb>cio<&vhfRWR8>-lVpSsqNK; zb%&KfLS%w8+V;Q{H$=dQdI|#19f^mXduoFMRb8BnIT-mv*Yo@9~xHjc)lz4nrpL*sw6-bTx zi73z4&7GUC%Xu5!Yxml9bL-95HCiF{ip2{+9>@)ARRBsior?8H+q>6PAtq`eK_<%O zRdet5o0=}HQ>>o?T)%t$F?Ba0jl0(#S2A?-&DXRbxtD8j8++!t7nGE7E!HuYj!hvl zU|$Fd0}7TpEMD7^*Nu&BbNBX5b!)sTWx*!1-23#i?~`*4bNl9-di}}qk@~muE(j;( z#BW$jm^p-;1_NNs1J7cCH3VyOV^hjErNko&)HU~z_Ox6}TcJ(hPQgaUopx9EM`BPW zkcW~O2#p*qIR_DFG;BIq=Y^HDDW9Z)5dECvfS8c?ju31tLdXT(Cr6%9uhsN=uHzc+ zb7_VwaX_3%E3S@w=Y4id2J6G@Kh6r;nFHT>7U>}9gCkbXl19F<+(H^j-$j9lf=tv< zW*~I-d5ORchA{HH55#X>}L8QE(Nd4t{7EqqbQF z#B0LurXK|I@Y6r@Q{G<_5Q(pLB+7KJp)P_sZ;}plaKRE~g>|qf(nNV9I-9?LLb?(e zCL^<^m#9dk9hsz%mqgyKOC2L6GB8yUl5^X1q$&qqguSe3nH_T=dXS6}rB!3FCJ+IR z^-;e3GgjCGd5!*uf_);VAPHSNK|NxL!D(xVo+DhmFOY^j+@uqAQ%D?`+-P5P(Vmod zUkF4>bpmu8*5Ke8-{~iGzpVBkD`k<<5cgdnodm~VZL(;}N)Y&)P5LzqdB19s*|LUi zA2qScb(JXiLHkBua!}XALEH3XY;v37E&@mgB(EpLaUkg*!Yml(}#8paplM~KT z&b3V?*D;wy)08s}lkV@EVsg)vge+wSdnP+NGMUkdw1lR~q(+i&LiI~}Fc{DV^For# zqE{D%199*-=S8=qeg3V#{r_EF;0u$TO={@e`7%EC@s$X|wRh`18RV@M3#FKau0*+V zgRvikj$eJUY&u7CED1{jZj9x^hUpYfOk=-g`a}$b`9y#+DD7Bfil7%^z^yPV4WNF% zVR}s#*i93xAgCZWF*R$9;TnV^iZ+WlNhb<0%5+#+o`T%Jwz{S+Tg#7%?_YrQr$w{+bXZIJZ5{_3~%9>BFvJoSur#G}IlNz+s2cYgDK(E=kH zA#QWDC#+ca?%g(5u3Xh?!_tAW_vF*>F)M59=KkGVY88Cz^;c9m<@@f{8~U3D<&bpZ zDv4_&k?Dvq#OBK=wUoilH(piCMYq=%Melsks7P-?$~&D(Xn=`0#F@NsV12_q3x!)} zrMw^!e5#9w*QV_ENyDNpkrniPlyu~P6CNYaEfnhr=b2Fj{z1)M&$TE;+zR<}5w)B! za){wzdphzBxuq>*AQ7^c7?nj7 z6A5s(hn2b{_N8t0k4<7AvJm%3Nqe)GlaHcl_wz#Qa-s`n%>I$L&4rA4Inm_`@;|jI z3NcEK`f`0dmkx7!cy>58kl|Ac*6re+zTPlm$dYf9g{5+{&8gL4;sR@qm^A0*6d2w z6jlpD20PLr!mrJia;9_A5t7+fWg-exkRw?jO3&dM{muIwf`Ln2UW5Qj3`}3$up(yC zS$Br@3b)NM}zBv5mFG-Sj09asLOu8qe8|#*H2bv zfqv!xbl1DT9sIwCeFuDZ??o=~>7EDNZ^38#ubBmRAD`|yl&d3BDvwk^5?zp4Y8wWb z8Q;I~7yd8m(ijHX{k(M@lq>IWa5Lb+`({1s=FOYtFaM?ghw1vJFt%JZv_0f4CvL0X>J7@$cf*131_&H!~fkcJROoSEgO@1#;h*2{}rvV9Yo zf#P#?bSPtA-+bZsKCkEBE+D#KrBX7YqiLT(-m+%h^`9Uf+`lIT>9lf!4M=Pv@IiIpS8&WO_tZ?y37m*7gUaSF1j4bKu4DzNsD2h-QLMVTeUt~=KQn^wx z$JK_}TBj{a-bWpi>>ioa`V0EJ(cz1xUpzfN;g08zE(p=7SC7of#*WB7>P+tKx5p$5 zgheAfPb5SK_r5qJZA_eKA9`Y7dL2op<)FEwmy*naF%5U944doi2P;1Ok!{e^tq z93LN;jrCP?QfrueR>pU^8f%*ji>BWd(ki4WmEc^*WKzQkfl5nTBG?J;n7F1=ED$J& z6rijwv^`C5X(S7Rg%xKO`S1g_9t_vMscTr0CMud>VWS|VK`JSNv@9ECqL;|~{Cl`$ z%4w4=X4I;M6>1hJlhPF-S`8sKfwf!?>AW7XW`R=C?4>Vcei?{m+XPk-j; zy{AtDQuG)9;(u)Z zUkG7_qebT;!3LchAH-dfCA-B>lK20 zt~WbowKBOjGq`IK{S#3zJ|PN1fZQDYju}bbE=wVvU_!bGm@yY9^ZnzgaPdRjB9mp) z85K>vn@}rPz1cS9V%{7H(cCUenPti*QM@Ym9SWg5FoW#ru<|baxF5I-V$E_Er8oqt z-8eM)l3gxx9`rjk(cKC%&RWY8F$zXfuOrEGcaRbt-wI@`PMNgm<3l?@sGiszneEkr zsW!VNC*vHOV>4zHa&jG^+fxG}OO_LvVO?~2DWfp!6M)dm{TE?Zzw+g*E(_&MY8;W1Yilm zrH~Fl5DNq_K>-kw&XR?42q~+IQgq7g?Q}CxXga)fVU?`an-@y7#*2fc4aLp>c>&2n zId-^q?YeZbds=q%8KMIo9N2wBE(#i@Px8);6T%G=%>dRwDZl$64Y=^xwN8{zQk4^{ zxDI7dVV?(rN?2?bvhTp>DKMzm^(6Mq1_W@2t^x<4rv|AyYbDAQYeCW{|oPnBw1hi%2&*1Kl@p;vc6^h!q0!&J8Qvj zfBpac*Ub};KVd%ciBBk*dj0j+&0qR2{*w7m|C9ez)BT_QXa6_zSN_U>Y5x8{_*EtK zKlFp&Z+_*Mf5|-g)HCLXfB1*JCr#idS-K8O%7VSz0PB*fRXb@ze`CE?Fptz3@{s=7 zXy9f!+k?Q5MMxHF1_^p`|NiX#q>wcZf$ClekRRkb1QQVaguLP|=-QDTcdP}=VbOPD z^`PDeBJ(cZ89)y~&N}%5kYQM`@p`ecFWlAk?M|{FSfx@y?KIA)r!H^j$nO!mU^kC!!Gg36^sibN38RzM=}ZS^b#)?LYtvp=tJy zFtlnKEJ8A1|K_`=e2;R=Bxj3}E=ZHzqsy~$2?*M8&?=hMl~q%3^~^@**rZmTlJp8; zslH|gLP~<`u|VVaUZ@+UA8`m2!8qnU#}6I%(|ObHw#=y4GL0_VIkTtSP}j=k(<4*s zrp$U-$YXmbLLTmby$j>r*Z(ebsYo9E183;`A`B zPqUFTZ{9qXvASs1R~h^QItZ?$068Q&%O`)}7iN+yKV_SX{OajNmHeG8WEUb>Vd2_0 zMe(ZDPP{9o!DhqRV)D`m7A|3hbkVeW;p=H2OU-?0m{N`qsGKyyC_uofx9g^~UWzVl zDZ2Ja+Z48PCcl=`^#rp3nef%2ViAIYMT*&EhyVjnKA+Xidr`XFK!x%J!D3eMtm}%H zSz|+77nyIV@XCl;6+LKvG4p>Op!{Mr^8-cEg~7fo;7-OsOVmQF16U*IPa6C}2E^Un ztLBL(pAkiNNBU@5(rg#~Iul-kl&6K_9p#Uq$=El0tAV2>2<7xrMrTLX_e$D*B zPkutnu6OY+P+kDz5f(}&wU`V%IjKq;ZjHyS zcWQfSG+N`yPvmfz+?WL>xNs#*2FgdH%hevJHAP(m14!mq)sg`SZh<22q7d@gX;qbA zIRy9TBBZu%?^1f7+#t$n;}!20xt2|eqX-t{>fp8+HAVUFjCAdm9*I@``W8IZBpO#NRL$a6NV-oA%X(JbwKVUMMw%IM*F`4cg zW-Zsy$aiBG$!^W`69IS0Mc}y!#sTDs$cGRg>Z~qxRy{t@{huD}A8Uj}BJi>Q6KD^h zRvFV9W=vY@yR?4Alq;*Ivc7FLR)lP22WESvX9~%>+05<>dAw&*&6muT)XS#Q`|8p{%Z;dTd^|$J!M$5}jjImZK!?7_z)n!tCHzxcQC?q&p%& z`w@Nev%mgz^IM;L)g;;jlQB`^IzK8NG1}R8kcFnsn8c}M0DEjT5ltoT0 zSAZGETt%4h0^|ob!SpDj0t14DP$XP?Q-ny^>}zf%V{)s~09x~BW|xIRMhI5FITkKa zzy*+qgM@A`UlB6lixA@nGj5FoAv0^Um7iX{Z&LZBH1fi@jH7@ob4DD+83_hTEs8F| zA5a+CHv(mwNPYFj;x0>m8H)mk0=+2;`xHpGJI(>kfAF{jU^(&w*Fg;~;Q{O5!vOV1 zeOj{8Q36f*^EU@Xh64p2>!SZaS@9p-$AL9~0}BMli75V9r`XI9CE1-19z4SlCoC7F z%UQrK6OgKVx8E=~Zrm^*`@|>B8*jYf{bB)OB0qjGTkIz)_`+j-!u7}x%4Zf4gv{Q% zcUQ?J*O7l@9tw|Ioa$92TVXDTfa|sXT5VHasi=j98U0z{o>|1t4>ovpR;4&NuL4WD zkSps#uqbo##-`wYxHVblfwRiKi+2|2M@(EmsQfd@Kl&{vt5 zLOzu%W%W=9f-(vc9ft%1WS|cdw2A$Ua91NDT@V@s1B*v8rPnx13Ceq`aV)yZ#7(f% z?VGjLl3eReLVE8Si0q?JHqB$Aa0qdMTydnsE$Aq;iDp#^)y?FF*-x&TMj}6TaPH0I zjvids7^FlWYfp$*U7o*G(q%)&M}>=5(p3?{wfYI+RU+SpdqPk>($KA%&M2w6RhVq{ z?;AA_$Tik%IReF|{E(;p1H10fzyH%gZD<-@CO6Y&kdnHqYziSRn&QR{k-=N0QqG%v zO6p7qVL8<>S68a0B>An3UenO!h4xp?dg3)x9N4hs<;;;urihxDGRZ<#NYeT=d2$5G zktc$fIP!Gq7AicjhNj!2Y#B2%=G$)^nP2_&ZN*%!Yc0t=BW(C_utHIVg2 zx?yifsPb7Trc35PSTw?bL7R|^G%{z>_k+!baTDw|E?zlHOMb{xUK+yy-_wv;olr>j zv1$#4Ag)|5OS=kZ$z2fu_>QNAM9l(LGL+Yq8zqyOVIf-v!3qKeZEbZ$Em#a1D9MXp zDoq|BX(;?yOpx)Ae|O!OB%K9bo_Pj(XV|+h3MjpPupbk9RRIQhH{_s?_(wiqe(l$OEp(vkji#-WVep4i1`K%g)i6LJC?oeSryPQ{b@l*k z*ghuxg8&#&L`;p3t|MEB03LP7D*!t+hqZHk7bUCY1E5b!0%U#!XhX7LkD6|Fo7?ya(OS=c*ihk!Dzg;AdDZJ<8xT-;8?7n|i1~IA0UZqS92RHbZ zPG)+dKnCUZ)I@RwK~1HsTe$-+fDkU^mXkFQh(_Dy-CfI8F8;l;1_@DElX)|0zG_D4 z@I5e9>224{-Nc5HsW@rQH2NtfQ+5OT3p(k0~y=}HLMa+WTLVvuyD3MNr}+6HAP zy+==t`uirA?rVhLy@L}aqka$`-IAxi=+_-3Pi&s+Cs2er#|nk@>7d$H0)_Rd(YMy8 ze6ehDRDx{=h-z!x%t+sqKgd@=IEqr{{H{x_e~+6(x5El zVN||q8bT+(@kUDtRJAMlDabf13}O;2AF#C5?$=HEYFWuz5Uew4?6pm9WrnHJg9^#xt|_eN zOn#mHh$bV8`^sdM%F87YEP%yLkw`6dW{$y;=29wEY`T|`W+?~voEb~vmJZep1qV_T zZ8v!F9ap_GlgA|imqDye%K$Eao15z*yw`L#b${RPsT3TrA2p!({KJ3v4^4Sx!~Ev& zeBONi3t!SmW6V4Ukx5MI6xTc^2DqO*50awAr6^;l!EEaI} zvY*vc(JB+Tz--SzX^yH@>S|C3B@RGNp9KTwAHB^=T`=XDXv_d)B8{Os@B;TA;8# zWfE0sng;L z?5}-Mt*n3K54}(Rrky9vm%o0`{N=y?&&@yojaSV-`4=ymzxv<&rm5Df?w?LWh~X)? zw70arKK|ir=1=|6kC~tNKm7r{=H=JICklc%6UhQ`qX;?>uHEz!YQyS`xtz~x2P9j`ly28-wPSz=$8B#BhtE;=aI-s(T6H0_rcH$`W z4--TU{KhxFVSemK|9uU3!SOxc_lM1||Ns7lcf|yLOBU;}sONw3PyUJd+~+6LVHgLskoVu)JJ!#jtDtsnLBBsVJ6r39jf`cGlryIX z{Y3pp3DJ27mIDEl%EeW)Ufwpv)Q)NP%I2-wrg^KqZAn*Wr0cz=NEgcdnMoIAE(-dT zwM&Q>qzkNm(2VjoOlEM$WE)>HyV=)Fadh9Lh9{*^qL<`m~ZcG1pfP%~tNd z5T{#eeJXTbG;6~bl|W@Wub4{mw#g;;M2@#jHZNr?dSbd#3MWvICqIFLJUt2o%5Qyw zFdaXb9Q@5hoSXQEK)HNC2>qnc<;Rz=bUJB>;#(7v^)G(wWpnG!iTOYN@sG=SezpDM z=_l6BPy7dez7)hnDo~qY8EzrQA z-8aR}g6Xy7_ZY=F6c)i{$f`ldHjZV3#>ko?gyjAEb|9&g%Q_2ncz9$7H))-#D;1qd zVNDX71ryzl@Y+s4f(S3@Pl-rUQ0x2+_f&Bj6(CJ#1VN zvhiKt{XO1a77)VcpMPEthpi16U~9+b*=L`fB8e$!PXxMHUza-0X3cxv^PKs;FMieR zAD);$^QZr$M#_Bh$)~h$eC~ID+x&s={XX;E-~HX8maNZz{&VJ)S6}u1vTX$Nv$npW z!z{%*gY z1>Bn!az~W0;94;E4hP=FaTb(HMU^%F2%zucodNn0&0>+db$#hmpZb)3E&}=$uEz8? z`li24@*5r>qy@R)e&dsL(#H86;&qm`{MY~b|4To4b`&5^Y;exR3c*0aF&*vf=YQ^J zyl?Ug`j}_U8ur^lPFSN=79F^5nj$9|_X(U7#GuzQ+0wdxM~;>XInRo2uZ5u5NU>JF zV6o5jZ;5UsO0X)Y0YQSY-RqdWWOyI}eGIqAG3>~gE3X-}MMj7S4=dVi4?-?&MbgH) zv(~UJ3b|Y;WTcl!o5m<_j=D0o35l$AiY6&KNm}xs?9@fMZ<_?mMO0v&hwzbLc}|jb zr!{$&jGslxU$G(sPTJ%~4Iy1eCOtefg@pag2uaJxbsBt6jwGx=WTuj%e90Ig7KTlm zIruwUu!oelrj?sQ>g&<~dBP%!1yjB!x*a(hCef7oZObQzlx>39amHUwue3kXUJ{*x zJdN`udGhCLA!N*#zJAv{*l(B@zja&t>gIaM+_;9`LcL^6r#mqJ^S}D0`A7fs>*kAJ zy{XmBHP1c0ZH`Ww<{RI^ij{mZWCoiHhkQopT0$0pJ3Z zF*(y`@1aCb3D6riACOLuOvgedpBBLq-z{?yAiz=57(`G?X@AU=;}VxI zuonqow>xTu0(}v#gnwu47Mt*HZC}$!Y>+aDNR;~&V6dwS0jX40#=#?;g>DS~jxipc zx(}Rg-%9}`bc0C&c>&?Z3FGY_G+l5ICL&aMyr6U}gfk544%Dv(D-lUgoAB?O=72zX z)bZ3)PnqxgzVGw?vVcetc0Jfm**}Zx9r*sUKmFt8r+)m;j!7DQh39?xGe57Nvy>k{ z5WHA}vE=xP>J+5Su+~ER$I6;UXPgJ*IWC0M53|bDSsYUB+>e89ua^P#TZys+`1=%P zrV~tO=u519O$qsY7w#8B8hC|)BoKM?%`gZ!Lt^J zjl>xV?>8O0y)7aZ1#-g?A@;*croDX1Ib4bpp#v>?z=Y)P9UQCM+t`W)lB`L!@+CE> znXT-xkTMxV%gEBxo!SMuh{CX&&?5}}z^P{9DeH)ZhMrE zE2mCV4`J9eF6+MM^6CzkFPA5u1!|dCUCF4MA6BRr-h0*j+W-7D^Nnw@cHXuH+yrZl zj`@#&_6N*g{PYiKBy7l)bST|e8k|(SW+1xvU-_?p!~74w{v{z}Z|S)C*wrZ;<-z=?9Rxy6j`X`xeG&znL0@&^9248&NkAqSm7O9YG*$9Z)} zxu4FSOfN3fqtHI-2>Cm2!a^WPbQTHr*l;s`2yOnx3yDRPxG*s@%nUh79cyw}Z^Ep6 zQy6?gNF&5qN!Qkv+1}ok?<-19g5ld)i_&N|h1hK=Q3|^IIhn2;b%7#`7z@q~f()## zZE9H%d;mXplsE_4H08x$&qCzjK>ILp7FSB#MhOuNjt&pT1TqYd3elS9#`>^<%ww{` zt;3oH8{JU`_hef8Q5+%%u_Bx@^%2KKiMGnMvGr3AVY76L0yjqxTu;WMc|K{g-@PBs z0M@+qVwCl}c>95gT3nZ1`v|s4+K>NmSfuSD!*MkRm+qW3b%dfFwb-<3X`u}Uadsg!k{k%K;C z$XYNN7h8ACs3Y=hh<{l zw6ZDFs$})>UmJuwpIRB3ej#P*qrBNSYv#4?uC5O|s8>XX?M&PX--(#zzUL=U-XtD- zTPb-e`pBY>96sF0f?#M?%0<;dfA|OAZ~oK|f6P4j_@?^19|rIwT4)VZAAbt5>fJd6_OJM-1HOm~1U$Kv@u^7xzZ2cYF8m znXcrEcI+SA_?rV4rA{|~`0Pu8H$xd0tiOURqcAWtiv=?ZwC~6^eu;pDtZ!^eTYWU# z7QtILZ+YLO*^11AeA71Kg8p%IxTodI{&lmW2?8ury2){^amo!Z3=lpikL4patm5kZ zaYjH0Q9_X`VHloeW*`7vLxQ)!yLiWe_KJ*!q&pI2+ok>Pzka8oK`r{NXC(+DERU zNK$%jI3Zp_l=$wqdJU>_qx{VCW(|ql!;%<%PeR&`khRVzV~%?{bF;Uhgv)eVN;twn zBBQNw4-HWS3i9M9P~POgm_Rwplb=8ZEo%>jKna{x&C8c0N>s$_#?-quKkc?zTP5(y zz@()v)9sr1kw5WWW@~fB{I{R|yg91(%>VAYpEIBQ#FM5b-#`AL$IOrXsqZrX#vgdE z>GdCoM&M*!Byn=czVqWQS`FKtd!aLT=f=3F$xPBzcFG#|Jq|MEv^jY*;8wW=pir~k2E~khlA?eW zi#lV|P$fqT04o;jFS6N;=WZcoA<7SJfdPp1P2JwW?C;$(?4=PK(qf%{KQ+*Vw&b&c zpdV#9i2MfGbimCqFGA-2z1!Xy2Dg;i-Q6|SYVDCBS-7cyS(Ye2Ai?C9nbtT;0B(06 zxVi6nAjNn}AwqZ3IfjV3WzvQ^oJD=(X`eD!N%MU1uq$zqMd zqWy<^*-tB*&6`fIYYz9rqn2?EeVH{GaTH402JVY@@7{?;YN!MIY5Q%QcgzC8bRhBs z2e=4ay?RxZTol@O@y-K!1n!`C7Q|_b&bs^Ji!bWuEd0&C`8Rdra^#8wYp&en;Ucho zzdSXefNcF||LmWcAOGCl7G@4ndl34-D8n~Y%ih_pHB%=Nl!AR zR=}6O^4rpvCyb@ns;cEG4sb2(x9cXqYP}sGSpb>BGj&Z@=N^iVNtSQOOT7uAUR6ko zj1{HRlOHJh8jQkc$x>^S91y5k;5y()RuGUXB~S7@E`?aU9Q^li9b_!MIy3{;d3mk+lawxpH47eO8vtz^4-AfyvU2NLbM)$RXX#mUv$eIY3V0k} zaU!c>F~Ef|j>}UN7<5>uk54@_?{20LH$~Q~FqpODENFJk$2ZvMXZ+?l6S^Ko$RMte z1SAL|BLmgRiH~?m*|4tdQBvv_Az8R~La+$ofB$~eEZsCvl-O!* zM8z^ec@ZYQUSEvVzKaM*y;8Mt+R9}BD;7~)xwc~J#V1Cqu5Rt_W^SwedY=(DHbvN-QL;I4I6E(jFF3hS4jP|YK5rBT~# z5k+uiQ}P)gD?%0%iL1JYR^sGKLa>C45m3W;;!UTCj?CYmI z%X8~|TN5Y`STdssR3e=h0wi2S_{4CS5$+@HESWXkQAVUgLAds0KQ%X8d$y7>*|p0jSfr0&94&l{F9On5q^MP>1tG;Rjb)-F%>zL|iktaU zM`2@fU^(O9dy+6|bHHY(G>Ev1ak-9}XB4yG8mmQsP$o3 zt*!|{k9PH$DpF}m3^XCdd?w(Yg#rlq>vh|q1PgEtZQ22ApVsM;(y0Qsb|yxbK1Zv-eEUCDI8{qH*#Y&u%sw zrwA5qmqg|Qfk!L^`de4XFBV4LMX2U-x#;~W1AbBDSf{-Zfg>U)MG~WVoI=z+JdgC* z!~Puw%GQ>hcj8@K8nn#i(5S&e{5gn*o603cH%psLF*P|#DO22*ab;7+7diO*Q2>(+ zOn~_Jk6acYKcD%`XOx)zjlc0XViB`>;2AuNXY%Yg1ZyUw4@SvFDws)>v=>;)!SyvK zlco^KP)0(gR!wDd*HpG{n6;fJ%=*p^C0KyDp5rQ!`06Irctf6(GRe&kh>~B?dq>Ti z#vJ~pDA0F3f)!Fl!PGe<*tc}fVF z=y8si`GLi)FG{`!cnCs0qGZgWl0?TGX0Dq-dd>7w>-x9pwoF1uZK8Hy5{I`Z2QF^z z)7t||! z43JnFg_Uz4Nd2=;QIK0iX^XFvL9ifNn8DWwlo2Kr7X?C860AP#^6a~JZ|iJ)E?+cT zS9Z**2<0%cb!u>tzd08<=)?p{I}-ufTtU*YH&bzEtrBT>fTR%2h9+6S3Iow4 zFwLT%T|5jx?jZcUe_jaru^Z}~HXEj|LC_!ryw6W4nJmDP$@@9ar=MVP4);q!taB}Y z2gzD2P(S68X*HTUF~N*FGIe!zU5P$n*zpjk`K+pOfD3cfAhRgw22R;^1k_Kq!a#eo zWf!9GEdl<%`HqEG?ppRVO*wEyhb(cV3o<6i;U`NRkR?b{c3sA&)s&1;5Gs35f(crR z&K-hYb0CVWJ<b3FMjchx?kQrITHr%;a;A> zvv?-Y<{jrnn7mR*{~Pp0F^@4I;{G&{g9$={S#+8wrd2;Sjq0AM9^E$$kvZAIst~j3 zsKJSmjDJF`^8Gz?y?A7jLs6_(-)oW;>q0*29+~06*G!`MiYR({8wkPv;(bm4QE8gV zq!H-VQbD6eBxsLk6=mG#?#-?aOm2k;iCTn;Q7@@!2_sr{I*Z7$Q^@)BfI-Wo+TS#p z(Xq)Sq>Q8pMt zF61e7)wEKNn|Ago)6BkLjuJ1Je(3{dfAF*tsl(x8=BT@6ddXGu;6w^rD!D5dr(>Ime(DWL|X0?(xY$ z zfikc*ofr6rqma2Uue9BUS_JNyTD_@(GuF3uOsQ0vZ>gf)FW&aYaPE_s-474;O{+b- zU80~mS4TS||A=TF3!+k4)AZv83K>`gXCe(}MWAk3gM@-kFc#YWJd8|VT|B)%q6vB6 zJYB@q^V-___`3t`7sB8tSb)D-Z{*Sjfn{CdyZ{z{P`(@M8=6nHbv%|v!L#X#! z?ZjK>xHw$MW~Ze44|J*Exd~~q`BcS54))vnM|_0W2{KH&-x%ZX^hf?lA5S`BBqav>5tN;)043{nHN%3G=$seC-0Ck_U!q>{T#X=s-+73H%SUf)efR)h{oIgAF4 zX#(YllOs%z0&#MPL!ibMsG1qJ`ciG|GnF@k&YYJY|OY zXUx57#f*win3Gn{RGV38XVQ12&GnkcLezywwT?`B)G}Eip~Fty?5tV=vr^8BhAX;Z zr)E+@tVZ3IjDvPo!lTwTwfl9` z*lU>%QBOqJB}JgbCYRKXNKcftQD2mnq1-PDK7>k-3KM_1ad zAHiqx_pAvj>e^YZ=E3zX|FsY?Ec=i&0;)uTbzs^a*Vfchg=Lrf?%cU$dOd4xL@8qZ zBA*1U0b5&F)T-roBb)_Gf|#6Pl4Ejko@VX;d-}WTYvVlH=R_Br`^}Lp2e+ooKRdoG zxc3AD4qXc5m1A-WS;Er*vg8q_dDbP!l3JG_Q(@L64{^v+9MZHX*aI@fHYbk&aM7jx z;K{K}eEj1dH^22;zoioyfVTV0GtZbG{J|g8HDI6p>}SobTa)OaLGT;ba4q+6uj?;7 zlV|gexR}wsLS+^*(N6rx@4hk0{~-2!R=)XIBIa_d7Tx%okpTz}pqSDuu%xo+IvSOgpx9lc}{gFR7OMDfguay^e= z0U;_%SUmKbgkX=ZQ}o~NiIp2_@tSe%B2aP_Lhkq*mqc9J{OGwfLrzZNbsJ~q04n|L#ZtfX#)W2$~!z-p?*3I3clo_Q;=CCd@ zNyaHZ%0iZOTuao|0wpxdY_1lBl(S3$15C-}bGY)cN1G*34v;8F6`%YT27agVc)uvz zdJe)b4^oTkc;B}eVDC4`liSh&=ss&grX1XFOgI%+u1Md~KWAcTfiKhzFHtnNZ{N~R z&bzyd$Dz!@3ons>T&%W ziV_5|VqojFEVFi&DwH1JR}RPDLu+#yyH~F(@m)09nS<_uRVbU2;{zpLX8~)02z$-~ z-P#QIiCmVoH`3YE5`lYRN75e8f-w(l`U+WWw(AWu4yxD3qTuS zyMQqw?sdTO%DO*CCQsFIM4eHFckbL)83wFUm(stZ$c2-e9FV1A5!dNGm5IOxeJJW= zg(WRED3A-1fg3ODt5|~-dATQ1h7H{N(d2@w;;4nzmUN`-Fo zJ>T;^YGnrW+28%$-&J|~9pCXCDmxqq_VxR}|NG|g#~(L;;!pgE*cLc~Z-A)HyMuC! zz!%7nuB6|Mk65$v@;(Ae@yXvWfAOD-=oXp5L}wyrM#+-4>8Z_xXJFXxn#9nS`H(e6 z9qGeKqQF`AE?pxOhhT{Ek8*!U6m>^hoDm^Nb^t^tnHHT$nxB#%A!N>y<@%-{K{r1L z8Q1ffkmnGleb7v)t6=vqYZ5sjm!;Esg?mo$9oGkkCRLL9$EELKz!5ywZV5CBpEu3~ zt!%?lM(R3a`bJ1gDsQB=r2g}!HUSiKT{BFt zns#+xiP}6U!AeTI9rcy;0o-VLemcu~u8irnPBb5U7FKt4`-4l1E*K|VI{_OYTFcsx zDlQ}^rj#qI1*5iSpwZ6yo)WqUn6bw?h4MRZJrj9jtrlxhk_g5D0W?}V7@j2$%(>#xf?GfievPzpt;ao7=Yue?1h%zo3fqv0E22tht!!1 z2y*l0TVsJq-({m_)>=T4X2Ho%w^1@><*c8%TwVsMvb@*28%iofZi+CM_7oJ*Veqe~ za-<8FSNdZ{2EAM+tM9@J76n`lA!$*;P9*7?{;9!2{)RAMshO9?Al$tUko0EL?z6W{ zkZwp8&z+}yM*+f=mII7^@I|)z)Bm<6O6LG&)7+Oa^byED6J#8-+BsN+WHH(H(n~LC z`@ubz?@UO1$$Id@KmWY8b-+Z&m%sdFok-w3 zfqF&3QGXi(S#_gtS=J9 zjSJ)|6%O*!fjqlo(j!~GqoR%tbMo5rQQ3oRqO90oj2VO$b!0JuroQNN}Q4^p}|kbU5i&9_WHw`=ML zw}l7KucKbOVe%`YD|n=gzcVGfG^ShMACoIS-L>Ek{o#M-;z?GVaP4e1oVn!9h8?hA zk&brXYpj=m2e(VCdm>6g{;WNSXc(ha;~5Va0U@R z7XfZxD0;P8RcG1afIOn0+9NCPvY~ohk!){yY$jqbSPvy?b`e z4I#8El{Iy7RD|}X%LfW{tJOJGCc6JusVGAB_+0hBbdm)^%3s^HlsK|qa;m4vq%in)862$6E&xmdLT)+;|5f`AbY9_!V$YuB{>JCNQV`H>&-{))ytlJ);@?@s`&ORDm2 ze4TsGeCO$%`|WOMXlNRw83jKSamZgW2%>-xqC^Ae=Math#UU}n_g7;S#}Gjy7z9B< z5YR-8q9`B`MxlXjpvQL}?|kO-t>3C$=hoSG?=#%F;XZHgzUS<-hpJVps@7AhYLy{+ zEbSyD3qZ1vhwqc)MCIMkkm+h&IvOiY4us zok!0lXux)1eOa2T>vCpv!vMxRMY95_0eGEQ8wJrB$*IAwq$Umm%Z;#B9WR#>D6WC5 zL86jKQxZIhW6EH759fUv;y6~nCIvj$gOaR(Jar-ydc93q--QQCsg8_}Oh}`xdCcK? zRGDkPu-lwPyTQ!t`WYADQqj8dDQT2WDlu9z ztqi9k0=!gf!C>i#>haX{QCVL+Z$ye`k4+!5-`IErfAv>? zWu9z6-TdmW{%Sis5#l$l;acwDUY@}LDT8=D`N>bVYj*@Fx*lQwpVxMTc7X#h(}ecL zd$L+S{Q9sYf(0Nqw&ztE@YE%&ed1t{g1`~1d*blHBmDZOtBspw zxpi7Dud6Lxos&hGm#ei&Y31hR!Alh*SXI@14&d`jpTLwqJW>!W0Kp*VZFnOrz!2LubS?g|3dHm{&Vy;?ESRj;ku z8m;Q;HIvC8*p?>?^&Zbn$e9PvT4Trcglnfl`$oz(8=e@|0@`~+ln8cX`3YI6E~gTk z-N4GV6 zMpLcckW#T|1PpR`{`~pQC|?%>Ii-O$VoV$HQ&cdD*{#~ z?l(IGi?tViT@ywn#(Sd9C}{Wa%Cg3ZYtcuh!KM)rt_S{i-Uo?dy5Gt&W!7*3SQ2hW zpIMKEN0v=l+1MKo5ox8Yi8|P5K%OvGh9i@Gsw-<-@MICBf}j&jlR88)*hi>g`_$||h%`zB z+Fl!554$Fdy7ZTS`ImC{-FM5ax8B-CDz9C;CU@O+mkoG4_OXvueRI|=Z3E7MepXDZ0PlTbP|})wP4FQ1r-A2= z&7M}GuX$Tj0zc%lSPun3B3(@-KjEM}Qv}D=_6(6aW$@}b+eAAKh#G0~)54h^h(~U9 zX<5c?aM#IsWpRUqX7qZ>5Hd-b?y4uNlpE_1EI_z+j15n)I-3pe1Hl4J6RgzN)NtFo z_e1Q`2o`8Xk+nh)g{savtbkyFwiaYBc$+1%e^LQK;To$ueXboAtY?B?L2&#c;6Xs+ zPeeaDiNN;iiKOc|$8^KRr6nak=WQAs#y`Mw!?dYc?eKis=rEZNpBCku1F7O!ik7o`>X+;HQ=x{i}K+|*@M7*r-VKI{=%w>Qd{2BlX+ zKeupHp8T}0j=pUTd2PgsWfs{&9tJ1yo6sh$E>DziXqb3VuINNg8%+onWE^6^!RN5d z0q60aA`|ZQ1PcQn&yycT23+gGd8VtfWx&y6CruY83Gb_&?j_Yr0x}%WC*tHNwJ3^~sYrXc`te`Q`d6S1xNEU6o_Uj&}?#A`#L~4??g&*lGCBVZQny zydO9cIM}=Q5{%j(60F|Hk8jIt*ijJVWy{{}6i`Q={p@GUi(d313w{v>2ZB63;VaOeN&tGw1j%Bv=e=@C*W8@XSF8R$ut4mr?~GRe*Iiv^fkid5ly)Zz0w*Wi>9n zXUc~c-zl)i(bcokZf3P9>rV(mFl7&t6Ot+bL4iOcbtk+^UHb^j z&wx?Nx7=DX4X?6N(FV>b8>6O-o->jP*lf5wxA&V3dqA+*3u`X}U4UATaM*=%LB?mg zr#8U4i5l4GabG`}oz$b9!F$0qp*`i-ZtMi`U@_V-IW?o5NZ`6ngSJZ;rk#zNgY+l0 zS>B7)3*1+45D>#fo+Ikr*>h*jYjyS7H6vi>_1xvlm&`b1+8ooeFmfRqkg!~qHF41c z)1Z0|!SJwYd66vp5wyJo>GD}2Zw!Diq8qNyIdbfjlBb7Dd2Bp5Hb>zh=mb&y#)8d1 z@f2X#?K*o6fsMS!PC3l;{v0 zpbOr!x!)iy3@8k`eZep|cI=pZ-}il=JnB)8l23l}lQxh7(09C21ifJT9|xZ~=j#IX zk@Jsu#3SUDuY9FFYsa9k@NJnDLrPIVuBc}aEz;vh>;2wM7NlaQ!;JeWa%L39v;_Iij3*KizF$O>xlEqZV1o>rFugYrs zn6wMnLoR{yvt#D9nw=iEseG)5;xm9eVbF76;3u|STd>!9?f0}-K>s@F*n+cTRCT08 zBx>z%@fsnYUL=!fnaT{c93y*L$yuhkYWLL>rmOhV81TXR0*TB>;dpdUAeH~ zvJZg^cK*cX1(K}8VoKn4(5s;ZVShN9v^Hf2z=$GX|BZeI6y847)J~_>f%XBNjw1ja zY1BXTM*JH6UT@T;T3Is!#!lYVYDF$7(K>VXjOyqRA*}VM?y<2?edXCYU4)|wr_%(BoKkhdYsOL*O3mNKx z7c0+ZEeZ>H9O(ql@m@(_-~D|(>Hr>gE1#&0FwF2=O-@c)@CBj>G8M>|Wg2K#$Y;J# zw0+g^dUbpkk#)vVlf^DQ>rk-#im%g8oF7jG4Eofsc8kJ3(##zVGP6U($2V3+2OSk-^$V1T7q+>=8&wD{^#rl0Pg4A&ab!@@u5nm{@9VYmkxRl#CD=Q<2GSc##QQ!5S;p0D?e$ zzgMj+Uez|FqBde%3S+b8*}`)eVl?l9T>#3$*E!as^~2C=$~r8(MB$X0>1oY~YCH*L zv*D@ZlTumSTY}?(XQ*h?1o!^^bvANK33zehVaX#|^=e&4)tJnxv6#(gh28!kYplm2 zK*hl5acNw)TkVuqc^kmcBrrivJKwa0_8vC6p@~YIs-&mhz1>v zuOOUf?F&)om~MDU4d6?cF37SHMmBuKOUq`-7^#qfiHRvC0~1|F9BUR?7ehFDNSL3d zwcqew!lOrySeY;tEe&yl7}D~Tr#(|1eftyS2~T;tl7umN>Q_C>&O?wM@u=J7iBI_| zx%m;d$>W~*R5#5|&)<66pQIC}yZhE-sFu+rE$CJNG9`m>-%I%MTvi^R% zJ?G(%yiFeY=-c(Tr|Y}!kS9OwYm^YOH9$^oeAq4W*gL*bZoK*7cK(Lb52NvsV<&I4 z_j{bU;bwbIFM`z>l|m0Fe@zzwA?mf7?SsYiQuH?@5NY5VJegONsN;PKy_$gOKuiN9 za0Q;Y3m48hPjABdN4#LQdb7*BxKm)T3y&HE_2|*#M*1LHwAtp}y?WKWYpm-U28hTJ znQnnomywOH)1H8V__bi2g~XN3i;=er>gsmtV5_b&szwdaArfJvnqUYA1Qs3BH|o0t z)T5RXoALfX?u-SoqWS(}cir6ZZ<3XQP4E(G_C6K8SHAuJb_^_3Lk{R9~O*4&*uxf zpnqEc{V)0#&%+?_YsJ#Qx-$#Bp$cLiDSeks8LUS2Gp~9W1-%=am@`tv2E(h%OBUFo zFg9bsFA7?eWwup`P_HaIc}9Ujuo>{G0rKI!Sc7=82G^0B?t?h;4r~r zwgQMC(>jlxIHC2D(QG%s!7K!(4-yn39EI-(&zC|OY@BnS{ZF270TaI-)EADDmhoQ*ZQ?(?$K1at{KY!k@r}BqiOJ!5r4gl~9vC(ys-u8vs#v_`iBp?sgRE7C% zR#x1UL1p9dDT5$0l9#cmS(%)kQyXKfL!9EssG^xX6V|2?eQ+2I)sy&jSS+}9BbBVf znv-QULit{pEJ%H6c(%Gzz{AE=r@dT<)oj$|!o8PLgI$bIk0nME3;`kJaM4qk@5#}y zy~i4rrgr#B!t}@$a~4cwY_2T%$qj?EpYn#k`{{xicGq+tHvZ~CL*6;J1rP`WBEkqd zq#_ASHM9B01B^J6_S6nU(fMtN3LHMO*Rz3jaslwH`H^xAcLo{QYXJ|~rArs(;)OGE z_39O^BUfcQ>7FPgZ6m zXCtn|>_rq#^IOt9#5-lC9@e$PhFG0C?TiW5a*d5o%F6PR5$&dutGedf16_bv-}JCY z%JCDY)evqOQ3H69cn4vI3H-u!wQ9wtCmz#lAN`oe%e)fTt%CCP`aeH6Z{8h3|AUYy z0Ls&^Ukl6F1489Sv_`EwiKKeN4X3TF%#*vcY#t(}2KEB)z`J)@b!IQXG^eHOi#C05 zc6MXHhc#LE-+!;>u`RROjWjFgWyP z^#_|0`|>>qV6VhtafgCR^Z{+aGoSfP`JV6j9$N#+@!jA3-FA-Ob_%Y}XuXQWOgk3# zYVanj>D1+jgWjDh01EM|TlAEDggpGAr zM25=9oOa9&t-#f4UCMJsWt_Nk_os8ptHQF@XPuqt_7Laqy=;bb5+FrW^TY2U6#{uO z2cE;ixmSro*ibU&l&Nn&r@x2S5XFpXh%CPE{1&^$pW7-h?3b2TEwnppX5wHC6oz#_ zW7H69%SoF*fOR}+D%T?q+~=R+{|*BBJ#R4jzfIB*rIWvoWGLCMu9*jHd1b}^5_p5@ zW2^~-kU_?nnrPvsm6&4uv*|3dA%F;Bw;^Pnbm9Ggxb`Doc+G5jm^S$Rf*U{%?!V{r za?j^KXUD}0=b~Sd0l^@B71kA1t4gZYI!|8IktJk~h}LH)R45K7Ra! zO*aI1tXPj|v2iK!lP#;Ti86JPn&_h5hk9!RQ!}z=heC@=g*!s zj~DgT2ZKo?T#zz!-|K&fc-;WuIsZAQ^4ty20|PkdQEr=SbPl~)z0s;`y$H&9zk%+K zjqTiiS%ZMK0CL5_=iR|JBG+o$RnhhqdkxU%GfO}V&b~YBe;MNKqA57pyc(TJ2SG3V zM#Ze5@}brN_BO=JNRtw;tq`t3P;A$e_s6QPYgy&g24x_wdmUD9m~j+hR0%6nDchrCohgIu>||%kU~X(if8R{{Aami=#7wY9Rvg3F zi(sf;WkQA7)Y$i@|%$Q20Z-BVT!bk_80F-qrNUPvkPWehA@JsXbsp3`t*3C*$!^izx@W( zxHb)4&q@Hl#RtS0y$y|HXJ;sBO^{Xm-q0rD*^n`&CNjE6V?ii-rYJJS3*((AiOz^$ zG;ahLSE9w#JOXq;2I$-|h%t=ukU++mwn#m_dUderaKnuHlRikxg>z@*!TY};moHwB ztCufWumwC?76?Nh7kb3JH|)#<_sRqJeNnDzJF={#GeoE(BicE!5$~D%?=`vL_3>at z)(;T$12T3&G1@jY4w-l$CrA~ZsF|5rsVWJC{33G*35N#+i|IjgbF*58BhEXhWi;$s zDkEu1&Iqu9$9Jc}dd^HPBaLcur$jMRFC9@lYWeaYzvPL)9OuuUGY=zVp7oWUaG}3E z1NDcT#Sio-JlwhDYY8~BGDLL4LE+b)B!%l824Ak;EWg%@zwu*K^~)b4s%QK?C_sPf zs+e2x#2#E|-3GWj*Rxk1~Rnb(qX zSdW!adq=X4=*QPt^)lMOT!6=JD;VuH7#Ay4NGf623H#1(?N&qeLGRC(Wwf=fI#$rO zJ$Z0H^orSDVKMl1Uz8<(eMG{32-LMdD(e;NJn5V<>Xw(6q%>2I{5ZQTr!)242Ga!B zi7rBI*&{GIR9#E&$SI`4E=^ysP7n8qLhY5_?IewF@H+(A89}|vi+fDZHtX8JxOPpI zy;Zkqa^Z6b!~j_tcIq92|01%gd975 z(xxLqFwEPfMl>E9KYfiUi{>fQ&Kdp1V8PI4Ee~ELu04PD0c-Sbd+Z%@`sPQ-9Z&j7 zIdc4zT)J@1rUgD)&)<66j2w8x)19tgK({PcE45O~4H{ zRE>MJIM8oAtH_POAViII9=KW46V`AgO%+4G!=oQIKVkb|9Uywj$mDi$5WQJ(;=>V= zA3HS7b`(eL$d=e(+5PpOIl4f3>0uGIqrc3Gf@u1qzSff3YEx>8#hdNvxN*=%i{yfbQhYkJ(6_4*WO;FD<=w;sdv_8v|B=&gn1d34XSI=%7%X%(4 zTB;lMMoDT_H9CXyoon@eo|TN`G(fc4rAe7@UpKE-e4SRiuIX>P>48a}Dw;7*z#v^X zg&j5a-}G_LbkJ~l?M?PzZ^l33G6(` z&ZrWdtpE=n(X#Xe5#ey%CAsfu_vxRu`~l_as@_w zOhkyTUcO}02Qi2UfluE*n$7FqQJZ23q2yWfM^5PbT6R6@T-Eddo;82uxUCJd&6O2# z;(|fJKoHY1@dA=J+A#>z($Zq;R8`)KCynTV0FN3`!XRQ>11_ImG5VCJSNC0P}z zEcGW^0C_;HneK?U)J+2)(W9-TH{a#W^MGLau>0s5(=Q2v1UVZ9tpCEJZu+rz?w=fC0Gk&O7bzLj||pcANbzkCi)p2k7dr z0$jdyN$a%gYotibGE$|m&kTSO?G$|eqi?|Vrw>XtHd|7ZW?PB)*b)2QY~`h$+gK;$ zb+2L5{u`2KU9$EKHfYye5rPUfs~07giRyN~DD8sUX&quh`g$v&ku)VOjaE)pGI?1X z8Q1r^-#U9)X*&f$V%i|Y2}0#bRxd!%m}cms6mtI{fb^Y^Yto*W3-S^{32J8fid<_w zLROTk9-Z&D{d{=wFE=m+FI$?D;@FIeXxYfpR;EPaF)B_b2U8=M2y5Lu*d#bto|a6j zA|s6z8J&KF>P|!2SMQWY`34J^!FsMFa4)3Jd$nj!v&C_>vC_%Z*L>Y`<-&zC)?fN9 zeY}kY22jav(D`cjAkC4c6lU`KO0d`ui>ZQDAEJ2=v7{uc7j}zadH8wOhTiQ}mr*}5 zy0QPzUVwlGOs~qRQZwy{_LWMu6>q?f0eR>_5-b^^{DRL(c4JU?4BiU>y6maovq#*h z3c>Y5+NlOSQW%wZ2Qhf@a4|h^Q}}=VN?F+Gre>N0qC<2$K07b4D-(#;^73_et=`A) zkPL_A`(WJA_2?ImwvDYs9)IPq6TmF*T4pK{k8FClj@#Zk2QZCx~fwH%H7@?UgVn!iNBwgt2mUs_WJ64OgzP&-D;Mcm+SgPZ9o{T zN=SSRE-K+k867Lhw30QZ5gy4b%lJsurU%-ALusOE#2G@ABc{`vHPw z!8y`4AZBo|k+&(Cae`&g#3ro;`cemE1%hHT4AfrVs@>OX=2d(~l=plwWstR4ohgG_ z1`sbuU)pr?desdKV53%6uNk@9E{5t6>AaX?QfXH9b+chGlG)e~Yp=t#S64T7PftRj zTu}BqJX;?WmBp$xxPu^W>|=bR=nQgV4{>l!{5lWbW51>RJ_wl7#xyCsEo0>|tuc)) zSGHY4D9`9K`HssOBhIE>D8kW-IQ)HsmXl$aYc*|<)-gso>1`CDXAm;6oia1{`}wPf z*Av^USjh*hu~MnZxORjk?OZJV(Y1pD&tRcgHXYe1;B|v=;$fqVC@=PeB7u(hjmY%O zyoFpR$|K|eBLOgo#D0GOqlHbXA*Wjb&t>f@(-Lhu;w~avbOd17dW8xzeGm|S-Wh6~ zXF6}oh#`m9b@~8LxHu1aWZx|YQqZ}nDI#RfSsC;rTpe%B4uZ51SZBLyvOFLz%jZ`% zlb?V9L3%oWD**wDND$9}`1r#UAnFT$O|(wDI|D5qipacZ{;w9R1*hRJQwsA$tljvV zD`wUB$QqHfzheoc@*x1^IX*cNeftu^06M}HZ`J}41cU*UE~t`GFPJ{q$emIGbwU~= zN@PYR^|#HW6oYs)S8#(;dHR^V8m^17zI0L67th&eUw_$e2(?~bU6(Z_cbxaX<%&wW z%8!s*`SDU4ofExaV>;m=Yq&}iMz-2xvr<-a#8g5E7t;x+l=MKl@|l{EB_a>j7G2;A z)?x9NBMI=GOim4dC0hN!d+=aowT>{QF%h+OC8o#Z^3_!(Tbu1sPc**VfbMkmsRSvu zx?kVK2Epq}$Qi`-)Aab<%p^?DjNk)ig&>+EXlIq+H=?PAg=q`rPFb`X$+}1Ac7O*i zlN*zKdBU#W4tn>3F%YttXkrko{oD%+6mvzXC`)bFFyh`~@yh1NXdx!1$F+2Kve(=| zdjxl6^hMFnh>!o?(AKcg)Q+7)fAi0Z?50HU4EpI|jORG;MAT~)TXfGfwPI1)AqhzGLu)EPt6=<-Llbtds6T)@kLHc0LJs7G`H=RgPKBYu3C; zOc4yxXw=NhJ1}~r4Pc+~yT8V~L7EOg5Cf*ek**||EE1m0`Y&{WO{{l{bmKX~YelGb zhyg}7dstz3kPsJKWZ6RPU%q@vR@5-S>oKW>l{IS^Cq6Aq6Rg$JN6>l!f`x|&g0)qW zHVnp3mSye2y7Oji0c_}sj3`$K5{5YkG6>5RnYu#>gDL}mIq+~XMeocRw_Xr$Duioc zVctAlaqj|a8~vKS1CJi{Vz1F5L6K+8$dDf8g?#5I&E=)2M`2d$=xko{Gx~QbC;6#S zDNg8LBT#
uBjw0Ib#xAxQ9EL&|m_Yn-%6t=j){!0OssaCbMD@M4AnN^t>b8BN5{H|)7dH#|MY~)E7_l^{Al;*?}q@KS;?T@TY zugffU_gkqblseumrt6u9tI#si1p&IHd`@o2Uy+50MI&9K8P^VW!n|t}?8ChCne|<* zn)@D&QFZ~(6`LeO=e9nZyHqdfh$_0C23V6dp$oV zBN4f3MllTlY2*Er3ELe&>?*akj8Dx=zO=pQ;9#g;uB?hZvVbRL{|OeT?ZY%d_Qk6A zFuYjjkYzgF4jGMcux`bVh(vm{C76++H8}yb~h~ufp2eid5FO=(*7M zd$N*LUrD1+Yv(}A^>5cP2%>jbz9V43`%!(cw6tPGA>OmOv~8|lNrU_0^ZS8~d?7Rz zB~o8C{y$S!u8;T51JR7H6g5DgmC$2g*k02`AeIL*9%e?4)*YwbNtSF z8?Lqc^mBMvn;Si&P{`?LS4YEVeJQ|Xp=IsKP}q)nubT6yZ5+)?ba>AiQ4^!X2HIi= z@P8kQ@E#oMpNF4$@KD6CX{R1g*X+Jc6@YRbtZcKnF=<3or_x}gO9@xA=IlX;7JHQy zZ19ivq$Vq?6&uuDTwd!CF3C+xvwWM>%6Dj+)sW29Gj1wjYM_i5u>bIIdD0bmx(Zr1 zrbpenshdj|U2TYuJ4RNv{kSUi>P61)ipJgc6XX!aiS!(^{Oh#24T8- z;kv9Zr^{e!VNy!tMakuJsz^Iev_{paU>#g_g;1iA0o$wHjJ2~&*BhT4mrEBebQ!-q z3(zk2O7=kS>;a5brfB)~L&Ji~7^XW7K0onm{dv&!Td!y8JwD%KV`{`-JRkkw$O2Q* zm@XB{+O9{A3Fi{?(ChsQ$Q}D;IS)hD!e93a15QLiJVZkim>|8tlajbjoH(xC5TVF} z=VN?Bv=$eaZLJmP$RP#-E)=w3yL|cL&JYoO51S2NhA2f-50Mun>q->LeqnyzJbp<* zNgy? zPvSQYrh2aEb&ix7FjVLRg1@69N+4R=XK_l?K(HN%FM)Shv-3~})BW&9L6-IkOpAP| zf$6G5&-9zHL%is39*Ti4{2;_2U|_?8X@S(kYSdoQF8Mln&*wg7e|w=YKCcGw>Oe+i zy|N1j`t$U6#?ZJ~uxrt+jJT`dZm3C0WGS!{eXV9y(_7Rc zt7thEjD&%;rHj^g0e&aF{Q_SMMY2ko&c+s}qwEBzq~nDNsnpe&2;A7afvg$>S~EB0 z_sr+hH*3X3dGz$0jqngMbvH<0zLb~ME95~!*=ZHl z>C-n_DDXHz0g375d;HoS>10~nx)a9%Fhuk7E7;cIwKMoyPr{JjCKt6omz{9cLdo$`~b!YdUox~72Auz06bQnbYUp$HJM5RhWFef-BgMzpTEB4>4N=0n=qrsYhDcy1{AJezpA?7 z`Ue|HY`CB*?pnvp(}$=^)QqO46;@$z27^1L@Svx z@?0sahUxAYUoKp*d$!pc-S)=7NHUxO=c9JHLq6i*nm{x|0dnOHGNyK=PQ%&*co}qL z(My(k4gC#~=4`{=2vjB(|68;=r*s+dAA4_piRJNU0GQ&f`w7!NKxDBGl1-1IAx=K z@WOK*o=nBz$x)nO0nUZzxh^~?2Y>M0U*qp%&o$QH`L$vb6XTuW9AVlHI@0RgA-X}} znz2&ZNQwt!g<~&ehz@4;ee|WnfHeB_ZX40d86XO$`S!$U-Wj6X&-i+^DvTS~b2rJQ)~!-&%}RTXAQ@+5;2;@$ zgiK?>2$p@uFjVA5mt|>beG|a~ZIyYw(bha=gzaRR)s07MufViCyfYS5V&%GdV>noQ z#NXY9-+#)>s?}Na83QvC1^|SLU>Zaatgl@+nOBxCcX_1%ddGE}rVJ{WG8nGQ+6t&1 zyuTlY@?_;Ecdrxghdi!lEu^*D&KPF>kRK~ZtELq}%Y9Ty6L!J9AA3LxCS3-nZaJYg z>h=n*AJ`-7`h82*fMXx@YX|95BD=$YCkt|O;rv-GWc{6Iq$C+nMp|~gZ8Bgx+yRaK z9Q!3%gh4mreCg6<{XF1>lkOZdfQ6H{c*@)zaXNI6KOhxnZ?)m-! z(&fB86DA`JdV)&~YScr{<`<5-GTtKMdJ{*|bioiz2Sm?^co<&8)Ga(gOgAH6+XWG|RUL8B7xVd|m8n~=*s}9o(=#&!97JF_*Y4OwgawR1?>FcC+1?$U_VN=}sXGJF*+SflU zi>oDR<>#eUU+D~rdXU$Q%7*Ea0FuzEu2`t?y#VB@DN1%(kEG?mbjCJiLL0k!eb>Ee ze%fBXJR`NWYn!I&dHCsieoAFqT-}Ie3=gHbHWVK=yvn31Hz8Aog)l#+0PAIv`XQaAVlf|?AUi@CkPh##M9>q7REdTt5hm?2$oN~{|g!D z2MB@J`GX8XahGP@+JOe`!Y;$<<(QV~oK3yF@y44hawFx-nkNPsD0d<|^4Wdo?-e84 ztE+29(C}c|NVayA+>2)ueP>&Msi_&6ojWQ=jvO&V`KFt0lvAfq%ZU@mZ6Jg4<~jxv zJe+rIumbbmEv5`!y?RaVyYD`^bn&d|=B(z6zXulDc0*A23bsb>AqsTxAb|b?&+cxb zFTujw91U3bwzwA{OI=~Ndw@V-7Co?`1SB9>?UowK7_@3IH`RD1h=wOvcoVXPF?&zk zSZ45p{kFW9i?v}_>c?a$`#4#Zo28za)@K%#?CAUOa`~Vc`hDQQ8N)!Ii6@QsG*{j8 zeLg%gxoH{8uS-)6ciNRO&;uZkMA-9SdNqSdy+9cAox-Tr5OebIEsbq2Ik##~W_QTr!Ko?mxT5^-4l2>wd@r#$`3m>{q?)~V4a^|jca`|lUMtf>@ zBKoxZ@crzT``dQ6ksQ?jO-SOEyqEL*HRCmAg!clTQ9vU@bhnk|>t-T-bhH?3p6M&IJd#WH=xfz zHq&W0lZ>YuU_g9P$J^79*nhU8|l>lMfsqi z_ls5m*CdfCrde|BK}Wuvvj}q8!iY47*o1g1VCvH3gd6c>x+I1jo=gT7O2wkeEcyND zVR$f5#fV=fB1r@(fpifKR@$h}ek}Cp*MzPKi8q&yy_< zbit;F>U$u~y+|0ug}uJcojIdMQpKhc?#NWFhcF21UR~aO17x>AIkJD>egpkOXLkz& zd9bvff?Pqg(q@ccY_2X{Qn{@;uhW1_GDh^-BTa43bI$%T0#aX=cIkwD4`Ch(Js?~h zkTbko5UwQB1>pkobF)ek=A||=F3XJt$xJ;~&aB;J(Fw2Bj@#5iskbGw>c|kJ%cd9( zOS~Aw_oRwv@GK(GRkL!neuK0o9xqKPOQzl(u&a?hA-Pf2w|qus=a0zs4}`97)uKU9E#LhJ_|YEKI#_dSBhfX2 zkN^mu4FsTo2REpDi$&@nEUT@3& zpE}z!f?yoEVs4^LI8*jYPJZ*%D$2)%P*a;(JgpS8Eh7P*$@s5y%Kxe3L zc#g>vdz|r{gS2LAFZLB<%`ejg{btJ#RiHBr=-@fuPi$q7Y)}kDumV7|=wITfvUgLp zA=$B`GC6lz)|RiE!PyT4y2z<9nrSLAD>}nDQ@f(vL}HKC13| z_SI@-j~lVN)VfWYth)ZddtI4!a39_)YRY#W5Pp4jLA`!w9_UQ)m6zvX-SE2+ybt zLz^gS|5+clvb-c~s}N(iZjSxB@V1T==VWY33I43s3oVOYy4(w+(W6m%f6CAgM$I7y$v=Y+6(V(&=B`F5`T*11^j-0M@!5apHx45N(RHLReoqKXf7*{RK(0!~ymsPE8pOPF2|25YO!HXW8}t=pRdju(n{gfhk8gg_Yq zzl!&3&5T^qjLxASY#T78h8m<0owO-4ssn_Q_BGa#E$R++dd~7Vh@S5@m>xP4gv`NLy!f?$bJFcvnga-G|Cno zH{-HVTQQrjAGohLJ}a5ZCCQAP)Os``?Ue_mp@akPg}Y|OrieAFE7o?8_8<&4mABP2 zW0}Le(wYuJWqV*uQwQ>D3#o-F*->o=$H(OAwMFw9;z7EuuWpP?N_}KbN!XO+#t2f8 zv*(SpR+UUWpuD4`?1(lf0MP<7YG6073MOE?a8y>c9hev|%eWFeWYTCCWVH66W@JK- zjX`08JLKzUrBW~2l)TnRL9fkfTca{fkR|d6^A!hC3v)%6zGk0m`$Ow8M2GY@s`|V& zX{k*Ov1O055tW;h>+XQ<1gMMsKpz9Apq&n3WL(>CNWFQRj7aI<>awh>4N_fKlCIT@ z**}eDOIobmQa#J7O;sA3iJqb&XsuhcOY+)oFf*d5GM>_Fl-HEURsYp)Dwg%WqRP9Z zL|1h$>jJl!>?&Fgnh(=YO=nBf#2~@%zvZ3U*DPBdCl7H9MY0;Bnn-3*wOEFj0 zglGZjKOVi4Ym!)p#Wcb7`dVVbw~OppRemyWwP>s06-6pvZ1~*0RAfhy>9X$HWI-N#+*Srqs zCQ~rc%{0;u(V;ylY1>j#V`LDbHK95{C*=w6PJps1(ODJ?U3H1Sq!FTozM^|RKXk;b z2V-p+y3tbO2Ev8GM<5LHg}!nP2J=oE%fnzauZ4la?l8=IRT@=t#iqr(qeuG!EkoKV z%6h**Ko`8LLvN+g1KS~3AWpK>h>;wgqkPE>@7l`cfjkd&y*5|SKD)IhBjuw;3}pSH z8X5Bz;DBd%FNQr&>mk83I;2Z~3->jOBP3k_!e!5l$QQ-h`}&+%hpfCe?SUq@779VvJyo} z%&Bccelk*(Oyio7vJN0zn!l0e88!IFg)M+E&v%yuf$9nvabCAn?UoqUDGn2u!w^rYiG2qos8pvWTDRxF^}bI7hQU)T3&jrN4(+9 zZ+-X9k*v`|+YIA|zMScMbyI%!H&EjxTQcJ1rWCSA$<~b%DnU74mTIG7@7ie)_88Ao z<@(0xF9fUKJlQ0$7K^>B_5y5hTw7H}ZyWd_`99EV+Q=coPW4%>ABZ zg<0?UT%MWmZhO`MX=2ejjbo*vBu{n3Uyo;l`Q7}EvZjt4r@TK6T;~t+Nq)7BGH{M( zaemO}d634O8sgmVqmq$k40(()j5+`O@GoGreR^6Ogz|(%HQNdARR~B2jp%m01ZhB@ zirV?Ct}NQ9e=p!wU~WAG3qu1^N_&DQjK;k_L_{}?X9)V@;s%)_EhkT&vZ-U9w5NfN znU1)NWF?e8qoV~S3>Y%|X;h~|(>r-;%)D3(6xA?}YrA6{LyBoH@C@|>F^ZOd7crDUYdvJU;6_6*v5&f9O= z5jQGJN-zk90FlV18vMPyToy<}EozKurV-k9DkC@puy>SfU0sxNsUV9>YP44yYLL&% zS|uZe)`QxSYkKqDQ=LgeD|b|+dRFqy>$2F+N~N)C+Xq3kWJGP@1aFq)re&mlRdTJ% z(r7a+IWLzkE!i~9Y;5uGh!U{%HPs32ry&g)wV!P-KL&}kJWAtk??XU92uo9xcg?8Y zlQ)8-Kk9G&r`d5$nrwF;ZCPKrVuQ~WB_p0Z*$jg=LFP0+BTAm&5QUm7SX*?BZbOZebN5^zGeZL}*4zoVbNOB%Tg*yz ziKzz|56Cq7!NYgJ{M7@)e)^^Ueuhu1<@3Q9o6RM*TKlwjaOlD5Ew4cr!&8Q(X_aX>&~Fv!YkR93a~Go97W z6eEuYlh0O8dab=fKaVK^TbQ3SGIsSU5!ZI!TLQ_V!`dy9MgEv_#eOusj2AkW@rfzT ze@4QMGTlgQMaw#GQ_-l9{nARFCML!#5JE`on9hZa2bpF{+ho%mgKLxYiF7ggO!@h! zTn7o+QdWwSc{NZPQqaGvm$feKl7UQFQ^xFnHV91rwFAW1>19}AAQ44ur@aj531v+C z(r*9&pgSCZ>$#8q?&wi9vQ%&V2Cf(ctD06idOX-nS)m=?ON{2b4Yln6!q#fErM}YI zHG+N+K$$XdM|-m$pv_^1M!&%X>@4~W>Kjv^SrZ81A5&wFK}mFy^^oBJ(^m1y?|Dhb z;KplE7*nHDZ8p}Npz9Sii2F?;Ybu#4jO%?$Dl)}sZNf{^Uc0P{Rluy(_DG4)07kv{ zsBmqwrgc`yS%-9GizdG$Bzd_!;Ueue@Sk_&gQ@+HEC$b*X4umH_VQv~rlyZ*n(x;R zU_~;8jZJKUr9FRCMjO{;yuB-_?0qaQ6#%{#AC zNS;&%eu^G4*dR|T1NPTKwkd}v<)dq7)zH0FM#fJ_rnKOW!mJ)M?%;b_%SFjmD?4F& zgPt%LkXb!zkumYcs#Y5@V~wFyRj>8@Xk*Edx6Yy8A##v7wOca!oZtN)Z{0nT6^AEX zc(@=)yl+$!l2f4-vyOD(t>7u$=Jf{hT1mNnH}=EIjccJ~Gg4o2hGBKBCYK)E*zaf< z5VHN)>7%Ln*bQX0(Wow};U2xy{!bgEhv}sLhJKKxl{*XmhHQ)@o_F&CbkP1h`OUKx~WJEliDQo2W*3oS<<}rnxAib@onb zOJk%gE2~u{TTF4>c>bWEib7g3;K)x%sEokWD%K~5=l2RGb4?edIGp+zu9xB{jZfK> z)V}#=O`MjMm7V>?W5=|{oR(L~NSHsIEcAKnm$Y%5xBS}_KbwMB(Q?!J(p=Z{xd8*p z(@3};_A2zGtYxl8Q_BeAlxZ?Bo|Sg-h`ui`&D@-lv?GqdDN)Oml~i{~+?086|NDP` zeNU3LXPXhj`m!oCHWX&P72(+NfRQjfV0?#!jVnuEsW2yXXN{ZugcEEnSRj7p^XH;Z z!(smToHj6fGet1OsCJI4=hswbZ8Hu*ufbFd*y$T5jbs6)H5Qs`_^emudTd0DftjS` zXuKz8A8$Z^v)|Lw5Iau=WBM2_*eh3^VVv|{I-E)Em@tNI%~O=!RyOw9{vu=NnDYevR; zA)g(UiK$7+<;!ZIuUmkHklaAP*7O6niLCa`>3%9*46NYOmWao)zv^vsw2R8 z{&#LL;!~gasJ43X2Kq3;<7}K(?et`+)B-&q!ogn-Q)8_s;?A+UJxS_bPo-8IM0kEB)r# zN$vFqB(r|6MG+Hx&|MHLV@dj2dFgD@qu5iS)#=+a!y;&MzF5t4daq zNEYQx$ZWhA7`r_Y=n%X7v;P?F4eJgtF3c!VgZA>J3!R3Gc8Q=N1W!oYbIYEYP&el0 zX5{*H7v|jq{o~PIpV(}9;pj0V#QmoE>^0u_rZ>t+P6+|i12y6Nz47#o(XV@td+xa> z`jiaXOv(##67)NUhdu0Joig1?goJ4)KptH_h|Yyk9O_QS^c5Yt;fB-E*gQ;(Z(CKUEV1B2?spn2~F-o?Qnr;V2DXY}9eO*|dwPFm95^rkn-AN;|aqHjBc z&wu{&9YYbr)f>Fth+SU1Dz(+i=E+SWOGJ4a$rYs_6&We0!O9f5>Un7}&`?~E^`(oO zjQ-sK-V^rQ#2c3@Pj?LXUT|JAJYCUryGRZdwK3Y2vobb%McSpCBvVr26oUnhSpJB8 zZj>Kq-jqy3?WmPcrxGph$*kWknfg`RNO?VTTympri-I^->MtXr0k)My8L891gx4=? zzZaGZdpK=^=8rrw4bVX`Xkd;J6PYJX{p{e^q9Nd&bO|8 zMuju3x;$plBeNN2FQZ?o)RyKhvIdchNL+g)C(rmqefK|`?-w`Kme%f|_H0pm*Ni=C z*Q}NLpsI>$?UC5DMa-rx6`R_%cdA-~wy|ePmDC7ABuFC3P|MXs;DAqZ9Kt$4&ZN3UiVt`>rkgH+Pr) zouu{M?wPBz&vRN0w#x1KVp!%Xj^d}!e)X^AglZHfwJ_a!?_BojnR_)fxTo5)ZtNZJ zRL4MKc6mbmUa=r{k-Y?I8RjhnQE1@_7+2VO$<+Z0?i20R@!%+L(N+tZJjvZ*FDJJN zB<&JXR*7YSTkYD@Ra26KRcBGubGe}wZljw+rixuo&NwEhR8R3;khmmc>7F{TVMHcC z`@;v}zUQ+aI;=iNx1dgsv5^B*l<&JwK-t?55xqj7{a}EE^IQO~Cgs(ZWpLlRvL8Cj znbsb=k9!?^Jk_GTUbX-02LS65ULFf1FU^l#hn;@gDjW*%JmQU-tpgcTPa&wFWUaH0 zp-}1&3_%0Jz8@+hV;xYTAd_j_GHuhd_a6kS-7DomCvdpE`Ciaw25UOP%rO2X>B-wI zp^;$PaqxH(WvOXYYxR(A9C^L?AlX{r(N&Rc+At~xhGtxhTMjH#_uWg|tN4ha&cfna z#HNuR4?iChA%M7!>K_K#z;hLn^SNgndM_B|4pLTD*2)Dc)C-CaqK+2F#|zOXsLOf4 zcPILKu#5r9HjMg-6;Y%aHufvwe=b?LHFmJeeh)^j9oDA9XK{Szx%~7Mh!8;VW5@z) z;w8`l;{b}q15o+Ps`WpPZ`+Fc;B?*vhoR-P)efDsnLj>bPQGDwWA>(jHud3$6)PxL z9r=c54`0#Qe-R5dpv#CX`)6*!8qg}G4pf%OFuLXcz`JuXo7aw+T)%n0(Dj}C+k%Uh zqxaiyrwk`O#a7o(C&tdoBh2=SW_%y)HHD0Bv{5|BeHf!t{x4OZ4vWsMOLyhXKgT`f zZLoXT@C4wrYjail4R@Q2yJQ3j|7@Tn+Nq%SExjkgHOz5rnz>`{lY&~eluI%Y+cprY zKOV0-%)$CLt7}lSFC;$9v-<)DJv#@}Sra7~2UKNS!Na^m^lB$$v!1PP#|dazhatv` zd937`FGh1OamvO(-rxm_QF`TxK|TH{42Bvh85@;4JEH$B-O!)Kc2r$SoKJO_ovnKE zl-CjHk98{_GjGX@&ahx+_Y1LJBJwa_LVjpO#s1{mn2rGLKeFWBP18;(OG)FFE^>Wm zUpnKa>S(#y!vC1pOHEz0ahT51u*RZRosgE4dLEa|r-st6c&p}fGq=xNesK+xoBO8Mm0LOxi3h0-udeEw{)Q^w zIkWa?C6iX_z6=@oG01rg_<(hCS8c@3HT4d2b55Gm%sO4G<-t8SKW?%=-*eJ;C|_LE zP&HYQ3=``I?L2bgOYT2-Jo&yi9n^ayH^$lIF%_yWDj_HHWBEpH7xYtI1|irq;_t-E zf3k(JJVv3|W{d!bncBPC?!D~yIdw6uiGO7_a=x_6uGA9wJ%$WbzkNYQ45TtKGpQ7W}r)BNI!*tHau{ z;Dbbo@d)Z#+TIbEC;=&`eCknLJ^V7VpBB3b02fg%-P(``6UWPg_sOGq66l}7pw>9> z;Txa~yY_+J<_C3vE!4DblKk6LNd3Q%&F^SyUTOm`!YexHBdj@MwQ3$fQ^%Dp_L<;P`DtCL zC%h@+ht6E_WY3wof7Xgj0h;;Ms=S)^udJVLC7DY*s_%}r6L$Fc zlQsvHxNefi(-W==9cpBr1^*t}rjWbNvCEwG(0r5PLMaM&zumui{4GZBUdr=+jq1Pv zC$7id^%(v*R7*0L!s)i zwTzj2nwOQ*rk8_~=4f*aMss4{lvV|VRwl%1nG06~F|Bv1WoM#d1s1DLPU1aU$7A1p zdeW}x1$(6})=?#a-&h!MdA!kt-ARmERn|}p$~g^Z;P`m1Z~eBNcyx56MvB@lQ*$58 z@6(@Js*47?PITJIL#UKGq`2MZu?>--jy5cr=9k1TRyvlE2r+=zjn#K;0@ zRPXr>2{mXufX77T3Bo!PrUbXCM|kb|lw^rdmXw#^yODrDF->RD z&wu6BgHLj#Ka%NANN4f-M|=`-XTQSMRDTQ2Kb$z1aG}*d;a;wmwYAV7HldUEQIvha zx$#k#ewKjN9c*m1m=-c_|7ClT8}7IhKD6<5HI{Db;#VwU*IS6}lID}3wcj;V{Jmdf z^uq5q^B4_eRF+4p+ePMY=1%G2UmYEj)S%7}nj@_)#q{4Z6A-)UdddA~!Ybtpl~9pXmekM4CJ{6}08E;Olr_Z44hS5x2j~ZYCXb ze{_!1RB3MC!K6ISp7>ZjQ;8mN(_r|<*QWvQSis!^C+p$YE8iZUQ-(KG8$#-3Jyc#O zO1^vHGTEY3BMZ>&nFQ+D-W`Wlx~xXBi_3-auCj671+eBm;;BFS+PgK_)B5!^*5PsC zUU=2jrF@-~CbtmL50_z`mMTju8-7O~BSXcZ zJwxG!KVTp7raNKHwxi`F_Wy=%4E}YocAfR;n7ba2`>Wx-BLrI%Vl4UHpY8ws=&j|{Opp7_WgHGh~?hWXmgtT@*0Jj%C~+9CYJZu0brv6%$i#Yq4A==IZ@iMFc0ik-hdw*k;6+KF`Phx~GrSG@aa>R9r(MK*VsB{YPKD9heJu4AbA8sq&dNVf8&eo6l zu3jbBAMb_9bYpo?^(+c9d|54DrBByrA?RRHNRZm=2A%ED*D_-=IV2%L0VB+7nO+<= z?8zyZ!`I2_K<@CMkVpn?sis?t+OU&HU3r3y2{X4vey}I8ZHy+peEdu@wvbC#=$o(k zz~3n5oDbnvj2ES2kA;~~05ee9cLgH4M@1)Hj9PShqH>SeZa*$~AzUJc*Iz^FHF*EG zhM2)Q7<=i!MKS(c)vRD-=U@A?+<0B>ZF8Hc0LUoi1SRxwg#{FYjG;z5U*Y<4uVd@k zl~k!zS;33x*E=pH*QqE5dkXeBKX|A3Rn@ik4o^G$%M<(Yds;#B%9XNgEa)QMpX4jJ ztDlu$R7cgHCuX%cH-)hu=SG+f{oYNSTM2QCDY!cKC{xG4gqOWmdtY^N?fX-18>v@4 zZa&-VcSi0BhrE9^;*y4^!6UUd4erAv0_TPf;g57`L9hK?VxL8!GsTEhoJ{%vgReJa$E--F!&T3YATy1ZH z$DJ-S~=r`PpsOGHO68^%7G&RlBgO8Au*ot#1MuxtoW8;~w z%n9F95U-o7S)61y67jKVEZAw0ktRQT;-%L-j9XVjXG^JCR4 zDl(a1u*v5vt?}MeM=idz5i8V=Mz6g}2d9GBHv9MfHoZzZ(tmJGF*n5S+IHXTqKrxtf0(_X2fP43h4#a@_|Zq|OO3b6#&BzX$L8mciU-SCtf=Dh5%W0ATNx z3Q`Y9USHaz+4_Vt7N+mm{gRKvq@e|18h$2f$YHwU^lC=2cc^vo5@65!fnn9+iuj6{ zNIB%D&e{IaSZLS1uV;S>9$maH;LE#?gr78S-8~V6n@Gj#-ZFaYW=<|ihf)-XgoEv) zb5GVU3dHY*DSlGs?0?e%)$=d&HcpPP1YFGP0v<;H6qeO03S8pX)^?gMUSxU}M%{+J zovyZ7=DNl|l(_j(E5ZqKbj(lpYMp8D4=1KMoKGYnQM;0-wfOm>U;EFlpy>T~D`HP= z{cy_roxB(*)04_n0_g+q%8CW&jw|3@G-F13rDdgsf&DbcwRo>Dn$2S$meTD64HO(@ zeik}4HNJ7D)wc}??^}vI13Euo3wd4s+>+PZrukFS;*Q_%{*?7|@8H@t<@>u%fTt(7 zc@HgZ3LW|#%hVkE@(wC_juhPptZ13LPsWkII=z^EobX?w`zQkiYf7KiV+r zHOTLUePbvlBW9@VK=T#n?iDMWizmZ_jfDI(3DCluL(+xG`(MX)YB`M!%Bn>&pF}Z$ z)alIs6X1&a4=%wy?7=4!CnR`V7p5I`ze++vKqKqwtF5T|mH%!Og&CZ!m@8yLhJ)43 z`0XDfn5P|en7VCN=h};=@D=42uEPmS;RWn(wiSK8`lsL8$PjLI&X`VkwFO-p?46q8 z{(7N7T;+g#>3HaAy?UU)+prt1=KXRW&jwToOoLlG<)@&fn}T!$-s%7S!M$&nF*gZ1 zi)L{y-dtuzJx)fw*F^9Zb65&|%d=iW9qiRpNZC4he%_Mg&Ee^F6!L_FGW8R$Irrz( zKU{cQQ1>Wy_4>cryvtuyx+6iw0#=pf9^Q9(E6UD?UNjU^8_wt~4cpAyYc=G3g%Br) zvysmIJB}uDFxuJXvWo7UPso7=n;Fl1^%{h_)$lQf(q6xEA(qw+BWrYH8bdu6{39=* zuRtZ-Le`-#IqDCh-;mULINL;~U9OL6+Kl{oWqKd<_g%enZA(w+=$!g?=41h0;A!^! zf6AQE@#p;ZbB95F$REBT4moDmd;w?NPNM9$LDG3Z#YhQ6;T&ApD?3_En48jRahgXK z0jX^tnZ1mjjj={^ObLiyIB;rz-D%bf@=$ss)Io50Wm`5(DX-LcFIxotv9nqEVA*mk z8;y@WxnZR37+H1iZ_WI4=SuvOs9-Kc>pyF|BJKag(;{R)!eg$NUa%x2S+0HjE3Yp) z!H1RlHuR0^G?eeiRGgyD95?im%Tzvbj(c)9|JdxaVQf^fG5hZSBH>WkKoT1Gna-mY zF}X;+rx4I0(^5jM`}{oc&7;?k^j|qy^UMDC+U|NITTDgp)!eR&IdON2-y?sev4dP zy;lVi;nud^ML!5uA@576vIqxhLv1&-t`u9OhYOu1euAuDw{K~SoDCfK%^X5}vUvLP z-1hd!N^J$Nbb&*@>VwkMc)q}~sCUvr1KreUC=^6*hM4}g(N+YQ5A@k*b})I_X}N6D z)KmzFEYXP!i{He|xy~=xkW`1K5q9nU+~alYW`^ zW6q?XE&1|uag;Y+6rnvIm9$&uWu|y))gdSSa)$p76>&?`BQ)Lm-2gJ_P*q6X4C!lA)Vm<;nf*ycX5(K`nquBwHV@N(%A$h^mGb0Q0S6AB zL9f$6<}H>NFtCG1ftNg;hyMk&`M(kw6^k=R>o7?Me5EAS=l;uiZ(7*c*`$H=<@F>; z!y8AMmi*o--I&R`WjxZZ^YGq_ozl;DEI#?*y)f|(pTq0QMvT0swB6WM6Tdr{mR&Ho z#h4;-vkf_rIoJ3tFPuT-a1SdP6aMGQG?|0-gv79gx@T@~xF8f-TyUXmh4VSX1I{!F zQ^D23o7iBcw{~@nqo=1vB$771yDzhXSI*gkRl^(F z>9>QZ>mxSroSDV)UL%<9Rs8*$a`f~x0#Xym-Mjtsf?A-hZ=yt_9k4V0j`>!;X>M%U zOT^7!VV_xb+pVh?--J1U1h_I?2bu}S(_5y0;w>)uiwKyLdgc)(%w>UKnVYOxPYgPR zj3W8eo5QZJWqPpbzQi*?PXneKPkOG?n7_t!>+JDp(bGI*pzl-nivVAO2595-e7@Zx zBV8HmW?Jl#87ZgxAwQk;d3hvX14x7>@W-=XV4475Z2e5_68!Ut{S5uSqJsPSYD0y8 zP+q|I10#;|#H0^bD=ys}Gh)Pwr%2y8q0`d>rB9^Q?|j?!GWzino$# zV{&zsn%Oaztl@3mU1(*p44YekW1qfB0FdLM{g9jQBO__B!PD={l^^YtT5YGC!{u7$I}*EVl4|rx z^nY+SXG@nJWBCG_K^WC0<44E+4L-fDgV$?Ft#c@8@@@3!ReEzC$la|6l_xa>efo=a zJypaJjvtw|FMJ*C)YV@Ix--;DtNPEn4%XSPG!(k`dVL{f0M4FWrSOT9Lz zV4vZDpYcms&W~RFXDR32_oTx6I{v)}((xOd!Gv8V@#8hFs^!_l*SncfnDR1YD*f~= z#&Sp~2cq63lpORZ^rd@$hvfM9_P4vNs|>@TZR1e+hfz^t^kw?Csa6RB9rQQ_EiG@O z=ha`7uk7e=0JNhnRtU>;wacek^BiYiSMusvd#}tM&cg5WwROGt-i3^|ww&L!8D?73 zn&esf^z9jZQDEXOW(HKM?VS7j-OgVZC(xcBm|^Wnb&VB8E?6kR^#Sr$=@E*&2ut!< zS359n3a8*`JLt&iBz3m6M3cF`X%yUv*vshkdDIl(9-* z4b6_bM9w{EKFe(r&g6Lgsu!(OKTtCw_1QKE_eNrgt|hrS-zq~ZUG!FCyRe43>y57l z;VP3BsyVQo*Zhb!-9%6E(u!}9ptB0JD*i@;95AQ8V`$gUII6P2t+o9H;loTo9ZAsf zu`H6`bEIfate^3{oF`n^Slf#s-&8)os-R?8?M~`xAi8_tcWbTb;;ekFAUH(}OuXK5 z?}fqeedCAj;Yke2_SZf1t_*=Hbs{LHCP6*K937p9hB@^oeW%zmAbUuBc+!)?3?%pv6sL3I%K+3T~zEr*?#5VwM>flQ}_tobHeLSK1|bx zZJWW~e0_frA$*>pPWn2*&_w+sH!7W6)%&2Kf{kk6pn;pT%u}7)V$+D!8+e~pV5H=h zS+#T&q>#G#!a@4kDM}V8UUvULh7Cuz@NRVuRgH*ro-hO*;uSb-8#w;PJ|_~!?Jdl8 z^*eLND%|f{G$>cLe>B5uDLLH0rRYF~j4sPR$h8bN0L9aX-LrWxkh>9aG$csicrO?E z|DK8MbD{0>6|&UzoIH+aI)f@os>%hDnPwe~ljaP_=c`pe<6{qhxX-0fB20s(wG{ro zo6ENzUNV_mm`g#%T(*+f{$7%jKAcefOS{9a=O`uiZ&<(Hfa2r4KUcWo>7L)1k<7}Y zS6glvuJTk`PaI0x-U~xm*Jeb}OW}^9u6WJgJo=hZbsFry6(ImW+ONRO&CM4KbdheK|hoeOvkk|oBfBlQES?QBc=V1}zeGXK8s(NcwoBWSSo z#1PaS8{W!jg|cy#!O~~LSbbRTI}qOMH1QdL1k=PZ|F>w^&g&<3ka?%VcEs^4P{8HCXb4&ldcTx<6 zuY&plz6d5oWK+58o8g;uk9q=}%tZrF2A1TdNYKZ$DC%Im(Yf`%IqmDGDiJO&>tpvk zRZU&pLu@(HLr+A*eWsc-Q09)W?7~ggre+o&$89^1iIiCvpI6Aqb1;)h@}mMt%|^8t zkk606@K@8%4Tnu+^CaKu$R1m=XMC^li*$K1sn;A7^7z*kaZS@DuPqr#s~F_3ubj81 z9^Euu!~H?X+F$+qC*tmZp6otBMixiDCzt$>k!_#Y5H?M(pr%Y9LzBmCRl<&-KDW>E zxbn%_`Lyk-zxIoUL_2yHnP+0^OSgpKDip(r6$iEmA^3sieAlENhW!e0od+~i&;4W} zO8)3r%8&L~bpG8L-YlA$WeC(yGJi-SVWtFUQSZ)PEFs=;R&IOetFiumGx$pIKs)^@ z^ek~R+LpPHOBQKS&Sy@5GG(g7Qk64nWbdJ*iBgfBO#$&pgp}vJ0j2z~D$sCXv^xyx zh0on|rJ&^j6&rP8aQarO?#(w*F%X-fVhrr7rh%2A3wTsMH5Don;Byy)d8?MeD(xNW z1UTYe5fu`-rLnmvK-@m+6a(PpFJf8A?0d35%bPcG7Efq@aSU59sK_2ro^nLs##!hp zs*q-ZOH`AO9U_{XhJ94gc55x(l~w?0-UFO4+GI-8r=&bTsfE*Nyr-0b`>9^ZVFaPc zm>i&cn&CGzqwv+^qAj}^dH4pEy9C&l6fyzMw(XW&DYjbIg=9$fh3x|J zApejI9)MgsD!|+z15#L1tvJ%(61}MyA!qV)6z=+ zVOL@dBb4pXvnsqBfP4|$&^vtUWxa4o)opb{;T}QaH&&00C-ABb=7-h1J)dJ@jrqRc ztlR##Se3z8QXU$e?B2eXsi(nxm9N|gQ9#yA<;AV*>IA|DO?tNZYB@YXzkh}O-sG!o z%bMa53u0H7fUfG%GK2#J-K&il(W~wAxMg>+I))Kv4Cwo%xRP+p&U1&8^7(T}@5h(P zbt8WWjHEQXU0v0tJF zeziDZp^sHb@)!tW>(iP6=oSA}!c!QD?1ZK|$~1O>rsov$-i3;%R!}>=;VjvEtH&tn zC?wVmRpqzz7L$&Nj2sdS36&C{LepCf5SGa{o8Qo z=X(;8O4^*-iF^h1V%oW@R0`8$&++f0yxjY^Bx^T7x5=9vKi`)^!3t*A2ie++XT*?r zc;{{z!93Q2)l?o;6dk3?&A&@MG=>les&T(+c!ZAKSSphIgZ>G2pw8)isd6Xe)R$o1 zyAs}YE+ptdu|r!|>6|#&CJUdgi#C%9kRxawH>7S*$>TVZU>cH06 zbpq6HKbXv|KBMPKW)E(?;G4aJp)v=wx1C%oQT_$kP3p(>cn^Z?YZQ8 z#6?X;%70SqQ}CE{0{nyEv;bun)KT!{-k-rRKO;|m-E$*;+pDT7&&A?pz|H_F$Lqtgad>0iu z_N=D7S=A>J_$6PBiq^xow6;Tz$Ej`L8DP3Ulp2QnM`BNxXu04;8~4i-(yTzm(FzyN zPi7Ni=P*Vd8-^L=&$&wAy9YTn;StO~hQKRyW&rFMm}=kxtMDh~Aea2ta=(ez$_2b;&5PS&qk zMXRM47#r%>gLc~ITn>Kax9xLD%4mZs7RNa%FZFNC+lR-;7!VKBx1^)c5`K|kNw{i7 z11?Ff`&?iDHPz)Fu{5KUj6l03s2qW&Ia)(Q!&6t2A8+)M_)USt@0@u;0?hX3EqX^^ zGQbD1=7F2@H-V?Ue81kp5F-VDz8dj>a%p*L@J1wYQvY~*FjAnSr%s7wI&G~*sAv8m zUr9O!)v+|7yHyR|b;}diia*Qvv!%>tutErgsr!tkiLqArF=B!*76*V0DPqWkGdfK( zaAJ^-gHJl!lIHg<3S~!*$=3{&x8uz;z8)XOU9nw)1)G|-aNW)fy}sifoYA-x_sus= z@#c)$2kOTZ8)yO! zAVzaqd?zhiU@KitKXpN8XsiiJeNSN-eXyusA}kp~X6Mg8J{MJbkr{8zB+P%CZadfX zblS}Qif+M2o5(+hm;I5+D9`!tA_6x=!>@cqUOA|r;_VcmFPD(Lu5vRIUn&#gYcOj2 zxGp_<;+~Q*=k7<-!9j?&t>w87Tn(o*Rs`TPvenO*xgfcr|D|=BHxln_2x@G3SP21D zPkG@_bGUD;eDSjah_RS&ugD4m0OuiGY+|hh*PojAWu2_9i@6}Cd7K3ikd*wPF}BnD z1!{NAd3yXG8r_^Cm2B1*ImjX2qKMDRLvpAhra*RLp!}nkMXGxh4dpQvR$fxV0v}W@ ztPMUC?fNID<&~mar*obSKvmv>Fky$BsuB|OLD_d=-cpw#q=56rb2qeja?Y0aH0*XW z>Ai`r6|KV_oCAyEypD&U`l;VB^0*^H1bi9bX}H)EZZfVR89<$i%1x`tDx{MGLCU?p zyfmb&zjOzE*-3m`SSfw1wRvu}i0e)8Ghu|CT&?(dBBZ7yv<)oScHR;qXd2UYJt=Ww zV2bZ(yBrdch^2Moa4)Do^+4Pz?9=p#1z~{P~eFRzzD%p_F%VzVH>6-v%-16hG{>6l6K_2n6jUGt3wpNO4{7@i=+#DkxOA@ zyH6eoFd$A+y@GRGED}89b$S2uFZ(e`j{&4*qkjB2TearK>y>sxE??o6VC|H89cQhs zLI-CBPDYD7xM6=W>*s3&L*K8Ei%8l@Q<;YGXG3@d?_H4C&*-TKx!Hmn8UL|L_llzz z^dgMKbTSSO!lfmw7(YtRe`8fO(d~NPgl1F-wK`-#ZwAX7%C6pHXQ)%ohwM1HQ~OrO z*$<%V_FqkUCUoRpPkAG!z-03qk~X||(+y%vl2y;ImE49AbtKb=-P?KL!@pwQJvlAG zcG{`xc=_Is#xuRD^%(vvL~uqW1f}eu6iA&PNpH?1^FSo16S#My8mw_#PB9Gk1eptP zZz{ev1ExPtgvQxM5=GQce*eU6_Ge^02*(SpmE@07MT{IKb1Fy{9~bXVMws>ZmD7w* z`pzDZfjXy_OHUvg?LvD(_RrQyo9K~0Ub$!S>T}s%k=XDr-r9D?ptkY3%^j^L%JtOT zppq9#e6Ks)mc2LnHc8dA4yB2<*<<9YS$A8r0FU_dO#_$yxuFxG{5+4+&7LKI&aoq{ zrIA05W+CbyU^cx!+QhdKh)u2dL3xWW%1=t<+rEDa&QU2iK;OtZN*s zhLcJrZ{>7%uTEczI2Uj1um3@NT{y+?F^@l%X{R@E!Advu;pv%o!~i2Q~ht2)-ng@j-5sR`MIp4A-KMd#+DwyVT6 zJ^ZJxE?6FJ5g4$4JVsxzJg`lox-2OH?ANb)qu}T z1Txj}rKJ{ze9#yGK352Gm6er6bzr(QBE2?kY;+#TwC2gj`(=B9D?Krs$;}u@Y0a7% zebtfJ8DrQ`N3oA2?gdg-A_w|`>{DD{+m|;Ar2bS)@)T6$#g;`>J{ZLr3x2drrT?g) zwDVTXc#F&)%JH9`W+c1N`RYi5dShSJ2DnAHgZtCx!pSI~WYP$4S&?$|E;N}`S5njv zyl7umf}D?-7!cMP<9hggP@^{$FJ5saN^Hby86Fxs&Q9@u{gzTjadZc{Gt`L1*t8X$ z^-=BA83mqPT?XuYo<)iHEXZQ_$){4gfCik5G}BiM%Ttz zczfU>?J`fzkXC;u!yYL%&a?)MfFw~9%2#dw3N0c(?ZL+0YUd$6N%K#AkD)#ZZK+q$ z#;d(vPqH5;2fk74UFEygV`u9Bx(r)gTJ}h-(ieF5gDw!|#aamx|0(>bw8bvTWxWIt zI*Pnuj1{;n>DhATz~*2OCy3s`Ct+qdoOyD%rhF!}g#)TG_tVbe=K&o5Zd=iwaP%DY znxzKP5j8&Uq*`$6yZk!{qGGd&_sv2&dk6c&N>)&d~nKY zJ59l~T?&brWXjX6w35w9tEPDKT&_N@qrB;)ZLkw_-Wrt&vEG1=4Da$}8^105hJGPz z#$^^zvn6SFmFimRPrsX7_FR7Z4y90QJ~``wbRP4To+g{EVI{VtEa$O_3c}f4&iWJC zK_w-TRH1Dn}@6>ro*F>uChk{17o}1WoQtAC{_nS&FVIsv55uRa}7C)WK*yUgk;d*?{ z-Ezm5aji~0%qs-wuSo-B6#_X`vH+miE7fjW1<(*lP@(NrDMK-V9DaOUA&Z9vQa=x7 z;F}{3h%cHL&m>{kP99FzY`1N-L}z8am{!RyJ^E}1wttm4P{+{!t2(V`5Odr>nHFOU zeeEAI@M99dm6b+_?KJ|-NJzjlsY&nw%DSeFtB%6k929697%NoHF+gL&G%=>S7NQ*F zcj<+te(K;7eocTwq01&@zrD}JSC#>x+Lj}rarTMhqk%4JGH$&oAfQdopT>7ARDr6> zd)lKui@NxYToZJW;OuMIeXU-yfp(7xw@z^a#~_d=a0GIDe7JoEm}mar!}VVTD9Yq~ z!8hZ7f}7Z!uF9UH%)r}7Z=S_)#^Jo@OSr2TSJbjsd%yuL2Kc8nbXUX@Lh}d8qKpr( zPPLZWNGi~N_&nQ}^#5@y(6aW4oR#(algd(viAu+V$!xbLHHSr3W7)ZoP-5Vhh@&Tl*K-y$I(vvEXhXW@$qDtxZQ|@n76D;ekyHpmDX17!sNxy*LH)eZxW77A zZ{55Vb}N5NRMTA3&@ke<-JS^C=h^wzc_EtLlDOSVy7jd5yyrw2mXFctIRvLcg#bCo zEsJ;+LvG@2o~NO@*85AVyHVs#Qhe8b)IxC1v(Y5|Jd@Qp-E08@cN`1axy?$#Lon|V zZaOv%_xyz!6zEQim)=bBdqS71zaqSXxUwdK{O^NFZhfj~Q$F))oX&+<=Oc?oQQXS)b=r)l3Aft^(<9wjqBTjmz8Np8K z1ll1a#^^ zi7BYj1LFubzwE!>WPvfEVfMHmY5GS`G=LcseG&%Kxh`xw-B99H!%Hszl6+nF4EEjd&Q7|YPFb`q3lI%>cw4xS+4 zd=xvt+Yz)LXU~UQzqFh@4}TtdscQ^7PI|O0bbu2J1O;`B-k;t&^4e4pa+`$*b&>c+ z^c&zGaTqE1&(zDP-ko4ekU+&h?|UDOKtT<5r`oFcHU<+$}M1b#i789J|BLpO_AeX&3LFODn8%x<# zEaBB1AI&i&XU4Sy+W^yeLM{waV;P4JyXnTm^GK_3RYW|u;UpVx6Be++;b11 zH%&bs#<6i}oPlu>xq_$1kaN%R<7KPwp?ko0kS^*uLp8wfWOV0BTqbqo19#Qwlc!}z zD+*KPm-M{6Y;EtAch{c%I`Pb}F1Wt${}}BNtSJfzQIr523Y?6NTbph&s5l+r7=z#C zg56y5PH0Dt8f#-(dM5i(*o@e{m=*Tj&TNRfDgFW^82_#hN4ccY1bo$n zV+Dp7Q&;Ald^}AY%!fDBJ+k zv`ecvfB+oX2y{gN4$JBar{?Q3&Sg%d9yQJ4U*+?gF^bI{#w51EGmB-JT!=DEnlCEfmEta3Zg&8T@~{pjNS&oOC|gBYH@ zY>Ht0Jvz*>*fVHsv&nCx2viNqDmYK@*-zA0T*|H94m^>`d?&|pSyqVB1f&p3p2c|= ziUX>o9Hr~QVr74XaeVyeXz}IjlkU`sFmXOYgc&jL<_E0)GT_Mb({GjbPdG!TjkFjh z@h?jw;(s+*7B0cmfCbW;>+yb5Rf(6kS6>o8$-G;(S+U52G5VJewNVBg-g->LL|w+c z#t4yr;A6shBO!Y{HMn(74__{N9@5Ev!{a`y<4;t!5^!4ti`uozyip5uhl1_kQmQ~7sSZ0!lM&ECGtxgN znpR)KG$n3+BLr*h0Ra@lCXw6ZTJYUgeKPP%anKAs9M?ntc${mEIjed}uENMWwrg`W+7CV)VQ@@LPC611D_VD8 z=~-kaLh5AdZd0yW~Mq~^?3bz!RWZwvR_)Nk1x32^GIInO&)P6i>irt;75O*)oASpIXmc;ibS#R1u^3QhXjqb<_&AHyxj?)?D5`Np5AI0E@yGn``5g|n^W^4`Ip{`ajY9hC#Gf{qJ{cS0cEv5i2cpC%@NVG< z?mFB;-rci5i;aUW_KO+E`9IM%#aD~N+*jdmb8Juz+xS~R===b_Uin79=oK#v*e@M2 zDwdf+ci#IUrWk)#uWW8snE9vs{&Yee+%;%u2lX~*QhSqe4uIR$`YN~8|M;Sjk@gHg zbEd*iVpN4~(>D}{*~w>E*y>Ac+iN&;?}^?PivC95M_purRP>Z?_F*p(?b5DB&4XD) z>+a>^rP)ZRNQ%xAk|{hv@ z`dmCW2hTqkjz_$sGC)A`C_|+lb{O+8OMoz;UC)ACaz76C*v?^Fj0)8xU zgbSvUi_ZRjv@^WP8PBMzB!VO-nG>@*CanE^0N`F=y{}6gTkMug$Y5)^n+%D6kJ)6CJfto!Vo1t(a`woEeDyXp( zXPEHm3Gg)5PLSXKD6dRZKHA-ay4LgyAyJ^=Kh=^5(zKqrOqo7II@>t8GRt0RLXh4?gp_gMSZvarH2wx1RE>)?hV6jAX=!TGI>AdkgXk!_Q4v(XDg7z42 zht8`KU}Thz?im;*0VkUkH0f@xAU*Qme>NtX>(KVA+4+p0b&(410FvHj28VuvXDBH4 zhqY2CrA~s=qD*WI#Y(A4e>YAJG2}#n!9O?#mB0R@Wu?{AXkI)r!U-WP2iPcbxlA9; z!^@$N=fN_^BcNb7pA@EuxHB7R&^)O;A+QAS$*%$BUOZ`~zw|`!=cq)_wbexW`B?E07@-|FkHW+uJMfyh;99fF( zl+T%Pwuj%jzn065*1<8LsM$C2SWQjv!ni5*=#7S8x9s@^tcVIEvr3zY zkss~UF7j&_Rga@E;Ht3w;!Rs^IXpb2*D6P`jft_z5tNPF#NQB)w>GqI8r?@}a^vM? zFy9_gc4>7qjda|KYXnDGfe5`#<5)^S|1sJE7szS%rM*rztE2z^k2@F~zB~7fSiQ1G z;U!OPb;UXak4(^J*ui$X1P15^J}D39SNDS(vN3NSVX{g(Fomd!7;r?bXk zp_@!`vi#Xzz4pH2%jh5DSe=DzH=CIm1$U_T^>W+^_ME46?5YF8CZGL7K}J`&tI;s( zXui9L+$eHK2N?~yJM^C&^@#8B28>3si}{s@QWF`!t{N4IKf}PZBU$Xb+<-cJ+)*^uZM8{&G;yo3)Bt2Im?Cb-V5r^*PKlk|z-lFnzw;_PAQdcp zlFa3eo>Sm=D7$25rd&i9rie#~kFx7LX0T@8*Y~uP{Xb=$XHZk!yTw(iiWCtey@X~1 zq(~ZQ1p-J>s!{|(BI+xFD4|J*h$OTK$blpzq};qS z_doZ}z30>UuxHMhbN1}n`&qxW9zgg|rt09(qx7#kSj-Uy=8oAio#UBc$z1HOatO6OhM4qDZ#C0vm%L~=#0zMM(Vr7VmjP>e=6iXNGswG51 zgSjln_uApLdEi=UBbMLx;@e1M2b-2O{8Fpce2?^UgghO04>aJ=Crj*7Qv4{Bi1cf5 zmd}23-+XK)d>s=WI}VD7Sc%jFu0YMf+{I{rG8AoMGUu6hGVTh)+S{k*+YDj{9FT|k zqqpwy$AD@Zo4)`g)m{SF$hr`> zmdi!0q-viio4MUD>A+5Y4(oI9<-QRF!ovA1A#T;6!}q?YPg-HfZF@MyrjT6KDO1$$ zQ(y?E2*fV5w;Ug3)Ok zTPYzR+R)4t=jltS!h~BbTKaN{wjJ%MVMwOzC&O)X%eabw7)GoT_W54%Bz!kqlA4jS z5!Clf8lxuy+MD*t$5X<&emA(C#W#llvZNr`; zOBhD|^O$w+WJu9aD?@LpGk?T2qRhd;FCnozzCRQ$^o}qk3Arfj?Vc?+3*mcv_%Q zJGNfSx=!#5EurNf#fgy9A0631#70k@lLS0HJkCgOK38?}7s{_upD*wV%LZY0)YP$X zyEbbYe{tN8q-c59GF15&RhG3M6Axvm$vT^Gj5 zt#@mn%+Wk_lHxX}iu1B4qqR8PeYinVpSs6^jAIYLRRW zj#ucuCF&JjjU5rQ(1b`^=lx>1y>z~X^7A_C?@kU-{F%4FGV;pi5>HnEZNyjkFvbTo ztH7)Q+ zre@#*j3~GBN_x`4jY55U0}b0PRve4eMSYk0|N^eyCWO3sbY z4-L1qWlj_pG@%Tap(LTn3kCkT;DwiCgVmV{;q@a}|3VaHo{C)JBMw$eU7I))FX<6_}gMbmO?xB=w9C*ou4xpCeSDgo6Q4nF9@Cf$WXl-80y|{(du$?3m@qT2R z?)s3}q1npc2PiO=^6fSDxsg;)B180w5(Jrnl!S9IcRIr`7ZU|m#@Q8e)pTI@^1VBS*~*ZyYP z9MRzA+!ryZ6WsN*nIr$i!JfBgWs+V)NREFq5*_=Wl^fqGYVbpO2dD?H9;s zk6jADYV6V^vuYNS*m z7cvq;8%`m(#?@h05TBo+|Hz)udT6yj;#why(^N$5jlT-Q^-cCX9>}`clQ&}~ra?34>fJfK;UU?8X0CUnafC}Iv zF8R4NC{I%*Jfx$8-*a~C#dzqn{Dxk z?H;H7ewsvs`IbfcmOV942}fcy-j01!BTeFA;(rQlfASF8Y_#Y!_gjE^jA|c&uS2aI z`6_{%y*NaSFaJfdomC2LIg^kUh(e^$6c@9fdV>yME$O%S zze+SnWH-Tp5s@LM>~EGLnoUv+XGD-lkB*@3ij1lQ9gatuZOVh|6pw|6T)K2$Gt(_p z@YedI=_Ap>Xw5t;mye!iJ70b)Ekm(1g0CslFk^kMh;r3K>K8Z;8Q0UI|Le=cYi?T8 zE0DvM@N#CaL;fwM*m}HX3!jUclOVU(o=Ltu0GelD-g3Yz`33xYNWOz?6+s{w+gBL2RC_`>xrp3 z6y?S=9xmEk3Q%x5!w@28_Q7;0kPJdQaaaWI4U4}?bUavLQ@CSyz02>`JjrtZ{G6Bs zcs%*n6WR0j-;4!_UOz{Ay1{SF_$)_yG=pvnsApy$9z0WIXuEfE3MrxNdM8m)K=f2b zNM6;CvY^%1ZDI+x&9ZMAhq%MJb4D)Y!sJ9*iHliKEFIQ2sH2lLZ((VNwygpQs|NEgQ(*;E;uHhP%p2R$27w~uq@InfXka;^;US6f>SSY%}vwi}1 zYNM<>qICRC()?$;gwlX1w{iYh1%6(+iRk$~T`rl@R)GlFGZlLCtGX7g=zA}uH}Zdyl}S7)LoiGMcQkdE-`t~TJN-qX z-rPN}1_kJ@sL7Dpqil*6JeJKvpT@twaDyKCLc{h=Bxp~!g{Ux{ zxe|snGc@YxZ3tCt@;(n65AR5#73_+o*%9~>7rHs|g)A3wnNDSzJG@xzCwLp-%AH=A z9VMP_MI887gh#XYooD=W%UZQ+!%yuRPs)wFH_a<0^0e^s$F}+OxZMuLp^Kih!Gjp~ zCApfB$ag2-y>EN=!CKor*2R63iSK@8%gx$dq`@}U3O{npIVUmaFtb|YyQpUZ6ty*o`89FRF@t$haU?;VW(;BJv8 z^?qCGU@S0ao?ZG`CS@;)zA82_0R`63C)l$4^hR-3d4?ojiN&C3c&Hqve|cF%5O!E% z(ZqJ}>`es$f%n=4&(tH+XmhKj$v`A|qa0o|bkJqaQFejCZ2HU7(+d?h4pLxpQ6k|w z=9BFQGwjxpteLIsqMyynAG*ikm*IxYUe8R+JrcSeLidg2aTVPAW1OH_gUR4HdKYJE z*8nM(fz~CM){4ev1>i@Hj{MX86&(FG|3c_4XhPhTWlQhly;|a}EVyAFgd<6)v~C z6;kz?OBC2O?mPNTKEO*@xFBDTFsA<%K{RD_1ia(~!CzGMLSYcEUv+u(3Jvk%`cHiW zeMlB;VyuCC&VN)eRA8->SF=zClW>-Ca#=rTbobV4Uyoe0@R z8w~;HoLNzlCH6KSL|vpFeAto8I98K$-6YW_{Y?N&%C7>|o_Z0lU0)&R23N?l_q56v zX$a3o#YyXq8DAh@!# zaEnk!XFq&f%-)O6wzRTI*%*<(eT7_2AgW>&OVq=r}+TF#OXby)4@lG1TJ8J`^SPr=InSV@{*>P9EuL$Fh-RUq$WJG zuMJR$V1NJ&z57ZD<9k4VH+^op@Xi~$+5QRF@vZ~IN26AuOSdk@R`>Hqw@bN+%{S^j zJ7#do&9me+NkuFvX|a0XI$7Vh5#p5dG2Q1ffS;TwH-vK7s9WIFDxx(BSmXjw<0+uKz%P`@;TQs4&$~> z$fUIig>eUY`VLf%-y{ZyyOR4cpC`-Q1#Pb7R;x0c)i*JGzvQhKm3KH!>ku;@L@tmT z)|ix{e{Nq)ej^seGV3W%EGu0p>7>5ld2V1YXxgt#UXLP-&TstMcOVqLJ~sxHO6?EL zWzST=i+dY-Hx#_Gp+bp({FL@?DA%0UjLZdLmTiCnL#&6zPLEjsT01d}Ki$AXysp}O zEeNO!Nc4`n4-+Bxbv{@F6m&1@{CaXB>g}8Uff`TUnJ34B+R-c051RbHW^J+O*Xe)3 z=QD&|Ywcu99VOy2gw3MJwh^=IY@gJzU%@2{@}3Z*6g4cbiuP(Nc=NYW7T~(AIP{ob zzl*d~!=VLzH8McBwb^HtDgKL3h zu^8}V-D#~ndZfuq^Dv(W1Gw`ThK2 z#tfV484&1lrz$upggQwYv)4G@_8!r49irshJ>23-Q+z_l zexfKX(LS)_SqqBVL|Q1Ju)TY&9#}Dj_$izg9xG)}miF_4bu7WxiQA6EQ;1CvLYYoP za0f1f&<&~w<*aR*x8k<%-t$D)LI`^yOOnu0#04Kb8YZN0H9aY%JTNWHfVb@6iv9?4rk%t_UE z{U*e$#xG=p?_sc%1D$gsko5m_% z3Tfq}0$;2gJ7<1xUepiOV#mgx#P;wOBqB~bFyQxu8$u|%6!R?jH=#9Ng|TV&@J|hx z-L%B+RZ0zux;x16%}P?=KFTIfg>^)YLro=D%7h52v-Z=6>BjxSXPjQakT|1U3OmHG(bTE#uu~*h%ao{AgCZ440Kx3*(vKTCcWOJKx}* zHQbJ$aL(zqIMnU6JZFBl-yrG^t|VK#T&>x?5)3gHl+hfz7*BHLAnXBTc@FHDY}$uC`O{&(^b&jq+8 znh<^+4+4BUx2HU&*g*!A^_|Bvn{TOik1$$X58|xZN+X<+IrGFX=$peh1RGsG`@k^5I}KI z)p;7ak(RYNYuML$L|ziIYV1H{2^g`H2FoTSEb7UBrZz-(U$WpWn*EaNG393TSl-0J zve$rYWP)nvJ4m0*UA^DC8(q`7OFbJO8*Wva<_|@Eyz~Cai;SkeV(lYhGB?U4)sU+n zjVtx*?9ZLcm{J123+&NDqxv<~w<6cd#qXFu=$Fw?eEviGM>Dvi>Pr5rZ}^knFXJ|t zt(i)~BYM6=)%t@mos5o#y~l3Am6gm>b$(sm5UpMggQyjU7rR-O_U*7=28QQL!`Gec&SqUV4Lta_Vq*{6*DyjG z``7RnxfdNSHs=@lzCyn|{YTB#j6GXcbOo>qtBbaz=k$+(>odA28~(flWh0cBC5H2V zEr({k(3%$tI?s4~Uu0)j)P;SB47#1m2|{8!JN+5=ckv$slpw2qF6^~x1dl!~;=y}$ z*c%Urj?xrw8E?R6*9xd4x7tf^rCRm}UH3_4E0XP?v`G3#7TRH$w6rT$l2UCP!p(wq zxLqEF4D*EB-g|-XM7k*AWq){F1z_dVJ-kfOPCTd0P@`glsN;0(tLj%qF$8!&Y?>`} zw97WjUmId=C7&r!x|WRT1oMe0eo~hUJV16R-PnafVUfm$iSdSH5G>yOH+7t%X!joa znKKSvnIt#QsIw^t-_O&YD(=jZL`-@h`%JHGs-2aaVu_`idoqW7$yt?Y&!fQ*E(mr(Xcwa(nwjQODC@xA^L zbkpnatN-LM{A;QIHGpk`MM|{ge9!JGJp| zY{P$s|9iVh-eX%XJU2fU6PHX*PG=D1nQ`w~^Y#T^F|l?{|9j-Ww*8KtHRC1w0{TtH z7jXWay&Lt(H#R4;Pnmgyjr9G$mx*`?cGkMsR;q#{W)jV;Igr;85$f|NyU=@hm%Ytp K>xT0;lm81G=0V#4 literal 0 HcmV?d00001 From 8c84db385c770a0128a558a4c5380baf3d7147fe Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 4 Sep 2024 16:20:02 +0200 Subject: [PATCH 206/206] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2bef9e1..b33c937 100644 --- a/README.md +++ b/README.md @@ -102,4 +102,4 @@ Using the above link or by [clicking here](https://osmand.net/docs/user/plugins/ ## DEMO At this point, there is no demo, but here is a screenshot: -![Demo LOREX, markers on a map in berlin, information about speed and distance on the right](https://github.com/Type-Stle/LOREX/blob/dev/demo.png?raw=true) \ No newline at end of file +![Demo LOREX, markers on a map in berlin, information about speed and distance on the right](https://raw.githubusercontent.com/Type-Style/LOREX/dev/demo.png)