From 6fd828d159426e076a04911249c358ce1c367d05 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Thu, 11 Jun 2026 16:49:55 +0300 Subject: [PATCH 01/19] added solution --- package-lock.json | 1608 ++++++++++++++++++++++++- package.json | 16 + setup.js | 6 + src/controllers/auth.controller.js | 205 ++++ src/controllers/profile.controller.js | 113 ++ src/exeptions/api.error.js | 32 + src/index.js | 36 +- src/middlewares/authMiddleware.js | 20 + src/middlewares/errorMiddleware.js | 13 + src/models/token.js | 13 + src/models/user.js | 33 + src/routes/auth.route.js | 21 + src/routes/profile.route.js | 24 + src/services/email.service.js | 85 ++ src/services/jwt.service.js | 38 + src/services/token.service.js | 26 + src/services/user.service.js | 39 + src/utils/catchError.js | 9 + src/utils/db.js | 9 + 19 files changed, 2279 insertions(+), 67 deletions(-) create mode 100644 setup.js create mode 100644 src/controllers/auth.controller.js create mode 100644 src/controllers/profile.controller.js create mode 100644 src/exeptions/api.error.js create mode 100644 src/middlewares/authMiddleware.js create mode 100644 src/middlewares/errorMiddleware.js create mode 100644 src/models/token.js create mode 100644 src/models/user.js create mode 100644 src/routes/auth.route.js create mode 100644 src/routes/profile.route.js create mode 100644 src/services/email.service.js create mode 100644 src/services/jwt.service.js create mode 100644 src/services/token.service.js create mode 100644 src/services/user.service.js create mode 100644 src/utils/catchError.js create mode 100644 src/utils/db.js diff --git a/package-lock.json b/package-lock.json index 288d83dd..54f9d5da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,27 @@ "version": "1.0.0", "hasInstallScript": true, "license": "GPL-3.0", + "dependencies": { + "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "nodemailer": "^8.0.11", + "pg": "^8.21.0", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.8", + "uuid": "^14.0.0" + }, "devDependencies": { "@mate-academy/eslint-config": "latest", "@mate-academy/scripts": "^1.8.6", + "dotenv": "^17.4.2", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", "jest": "^29.7.0", + "nodemon": "^3.1.14", "prettier": "^3.3.2" } }, @@ -1487,6 +1501,16 @@ "mate-scripts": "bin/mateScripts.js" } }, + "node_modules/@mate-academy/scripts/node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -1968,6 +1992,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/get-port": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/get-port/-/get-port-4.2.0.tgz", @@ -2017,11 +2050,16 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.14.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -2032,6 +2070,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -2198,6 +2242,19 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -2620,6 +2677,20 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/before-after-hook": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", @@ -2627,6 +2698,43 @@ "dev": true, "peer": true }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2690,12 +2798,27 @@ "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==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -2715,6 +2838,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2776,6 +2928,44 @@ "node": ">=10" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "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" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -2857,12 +3047,88 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT", + "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.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -3038,12 +3304,12 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3117,6 +3383,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -3175,14 +3450,54 @@ } }, "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz", + "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.0.tgz", @@ -3207,6 +3522,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -3286,13 +3610,10 @@ } }, "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==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -3301,16 +3622,15 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } }, "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, + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -3367,6 +3687,12 @@ "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==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -3974,6 +4300,15 @@ "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4022,6 +4357,49 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4116,6 +4494,27 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4161,6 +4560,24 @@ "is-callable": "^1.1.3" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -4200,7 +4617,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4251,16 +4667,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4290,6 +4711,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -4398,12 +4832,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4464,10 +4898,10 @@ } }, "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==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4494,7 +4928,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -4508,6 +4941,26 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -4517,6 +4970,22 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -4526,6 +4995,13 @@ "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, + "license": "ISC" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -4570,6 +5046,15 @@ "node": ">=0.8.19" } }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4584,8 +5069,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.0.7", @@ -4601,6 +5085,15 @@ "node": ">= 0.4" } }, + "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==", + "license": "MIT", + "engines": { + "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", @@ -4635,9 +5128,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "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, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "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": { @@ -4816,6 +5322,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -6673,12 +7185,67 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "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/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6740,18 +7307,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "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==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -6809,6 +7424,36 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6837,6 +7482,31 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -6867,11 +7537,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "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 + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -6879,6 +7570,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -6916,6 +7616,15 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/node-addon-api": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz", + "integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -6936,6 +7645,17 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6948,6 +7668,96 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, + "node_modules/nodemailer": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.11.tgz", + "integrity": "sha512-nrO/pDAUKl+wXX+lx16tDLbnm0fW6sK/x8mgohaCpg+CdCEl482bD4tCuAZk2DyliruiNTIZxRCoWkDqJEnAiA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6969,11 +7779,20 @@ "node": ">=8" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7057,11 +7876,22 @@ "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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -7205,6 +8035,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7262,6 +8101,107 @@ "node": ">=8" } }, + "node_modules/pg": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", + "license": "MIT" + }, + "node_modules/pg-hstore": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz", + "integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==", + "license": "MIT", + "dependencies": { + "underscore": "^1.13.1" + }, + "engines": { + "node": ">= 0.8.x" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -7362,6 +8302,45 @@ "node": ">= 0.4" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7437,6 +8416,26 @@ "node": ">= 6" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -7472,6 +8471,21 @@ } ] }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7492,12 +8506,49 @@ } ] }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/react-is": { "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==", "dev": true }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -7593,6 +8644,12 @@ "node": ">=10" } }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7619,6 +8676,32 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7660,6 +8743,26 @@ "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", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -7677,6 +8780,12 @@ "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==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -7686,6 +8795,144 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/sequelize": { + "version": "6.37.8", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz", + "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sequelize/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -7718,6 +8965,12 @@ "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==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7740,15 +8993,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -7763,6 +9070,32 @@ "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", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", @@ -7855,6 +9188,15 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7882,6 +9224,15 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -8096,6 +9447,31 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -8195,6 +9571,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -8297,11 +9704,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/universal-user-agent": { "version": "7.0.2", @@ -8319,6 +9738,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -8358,6 +9786,19 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -8372,6 +9813,24 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -8580,6 +10039,15 @@ "which": "bin/which" } }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -8642,8 +10110,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", @@ -8658,6 +10125,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 5e195a15..9f436e08 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,12 @@ "name": "node_auth-app", "version": "1.0.0", "description": "Auth app", + "type": "module", "main": "src/index.js", "scripts": { "init": "mate-scripts init", "start": "node src/index.js", + "dev": "nodemon src/index.js", "lint": "npm run format && mate-scripts lint", "format": "prettier --ignore-path .prettierignore --write './src/**/*.{js,ts}'", "test:only": "mate-scripts test", @@ -18,13 +20,27 @@ "devDependencies": { "@mate-academy/eslint-config": "latest", "@mate-academy/scripts": "^1.8.6", + "dotenv": "^17.4.2", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", "jest": "^29.7.0", + "nodemon": "^3.1.14", "prettier": "^3.3.2" }, "mateAcademy": { "projectType": "javascript" + }, + "dependencies": { + "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "nodemailer": "^8.0.11", + "pg": "^8.21.0", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.8", + "uuid": "^14.0.0" } } diff --git a/setup.js b/setup.js new file mode 100644 index 00000000..6c67a69a --- /dev/null +++ b/setup.js @@ -0,0 +1,6 @@ +import 'dotenv/config'; +import { User } from './src/models/user.js'; +import { Token } from './src/models/token.js'; +import { client } from './src/utils/db.js'; + +client.sync({ force: true }); diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js new file mode 100644 index 00000000..7108bed6 --- /dev/null +++ b/src/controllers/auth.controller.js @@ -0,0 +1,205 @@ +import bcrypt, { hash } from 'bcrypt'; +import { User } from '../models/user.js'; +import { userService } from '../services/user.service.js'; +import { jwtService } from '../services/jwt.service.js'; +import { ApiError } from '../exeptions/api.error.js'; +import { tokenService } from '../services/token.service.js'; +import { emailService } from '../services/email.service.js'; + +function validateName(value) { + if (!value) { + return 'Name is required'; + } + + if (value.length < 3) { + return 'Name must be at least 3 characters'; + } +} + +function validateEmail(value) { + if (!value) { + return 'Email is required'; + } + + const emailPatern = /^[\w.+-]+@([\w-]+\.){1,3}[\w-]{2,}$/; + + if (!emailPatern.test(value)) { + return 'Email is not valid'; + } +} + +function validatePassword(value) { + if (!value) { + return 'Password is required'; + } + + if (value.length < 6) { + return 'At least 6 characters'; + } +} + +const register = async (req, res) => { + const { name, email, password } = req.body; + + const errors = { + name: validateName(name), + email: validateEmail(email), + password: validatePassword(password), + }; + + if (errors.name || errors.email || errors.password) { + throw ApiError.badRequest('Bad request', errors); + } + + const hashedPass = await bcrypt.hash(password, 10); + + await userService.register(name, email, hashedPass); + + res.send({ message: 'OK' }); +}; + +const activate = async (req, res) => { + const { activationToken } = req.params; + + const user = await User.findOne({ where: { activationToken } }); + + if (!user) { + res.sendStatus(404); + + return; + } + + user.activationToken = null; + user.save(); + + res.send(user); +}; + +const login = async (req, res) => { + const { email, password } = req.body; + + const user = await userService.findByEmail(email); + const isPasswordValid = bcrypt.compare(password, user.password); + + if (!email || !isPasswordValid) { + throw ApiError.badRequest('Email or password is incorrect'); + } + + generateTokens(res, user); +}; + +const logout = async (req, res) => { + const { refreshToken } = req.cookies; + + const userData = jwtService.verifyRefresh(refreshToken); + + if (!userData || !refreshToken) { + throw ApiError.unAuthorized(); + } + + await tokenService.remove(userData.id); + + res.sendStatus(204); +}; + +const refresh = async (req, res) => { + const { refreshToken } = req.cookies; + + const userData = jwtService.verifyRefresh(refreshToken); + const token = await tokenService.getByToken(refreshToken); + + if (!userData || !token) { + throw ApiError.unAuthorized(); + } + + const user = await userService.findByEmail(userData.email); + + generateTokens(res, user); +}; + +const generateTokens = async (res, user) => { + const normalizedUser = userService.normalize(user); + + const accessToken = jwtService.sign(normalizedUser); + const refreshAccessToken = jwtService.signRefresh(normalizedUser); + + await tokenService.save(normalizedUser.id, refreshAccessToken); + + res.cookie('refreshToken', refreshAccessToken, { + maxAge: 30 * 24 * 60 * 60 * 1000, + HttpOnly: true, + }); + + res.send({ + user: normalizedUser, + accessToken, + }); +}; + +const resetPassword = async (req, res) => { + const { email } = req.body; + + const user = await userService.findByEmail(email); + + if (!user) { + throw ApiError.notFound(); + } + + const confirmToken = uuidv4(); + + user.confirmToken = confirmToken; + user.confirmTokenExpiry = new Date(Date.now() + 1 * 60 * 60 * 1000); + await user.save(); + + await emailService.sendResetPassword(email, confirmToken); + + res.status(200).send('The message was sent by email'); +}; + +const confirmResetPassword = async (req, res, next) => { + const { confirmToken } = req.params; + const { newPassword1, newPassword2 } = req.body; + + const user = await User.findOne({ where: { confirmToken } }); + + if (!user) { + throw ApiError.notFound(); + } + + if (user.confirmTokenExpiry < new Date()) { + return next(ApiError.badRequest({ message: 'Token expired' })); + } + + if (newPassword1.trim() !== newPassword2.trim()) { + return next( + ApiError.badRequest({ newPassword: 'New passwords do not match' }), + ); + } + + const isSamePassword = await bcrypt.compare(newPassword1, user.password); + + if (isSamePassword) { + return next( + ApiError.badRequest({ + newPassword: 'New password must be different from old password', + }), + ); + } + + user.password = await bcrypt.hash(newPassword1, 10); + user.confirmToken = null; + user.confirmTokenExpiry = null; + await user.save(); + + res.status(200).send('Password changed'); +}; + +export const authController = { + register, + activate, + login, + refresh, + logout, + resetPassword, + confirmResetPassword, +}; diff --git a/src/controllers/profile.controller.js b/src/controllers/profile.controller.js new file mode 100644 index 00000000..998840dd --- /dev/null +++ b/src/controllers/profile.controller.js @@ -0,0 +1,113 @@ +import bcrypt from 'bcrypt'; +import { v4 as uuidv4 } from 'uuid'; +import { ApiError } from '../exeptions/api.error.js'; +import { jwtService } from '../services/jwt.service.js'; +import { userService } from '../services/user.service.js'; +import { emailService } from '../services/email.service.js'; + +const getData = async (req, res) => { + const user = await userService.findByEmail(req.user.email); + + res.status(200).send(userService.profileNormalize(user)); +}; + +const changePassword = async (req, res) => { + const { oldPassword, newPassword1, newPassword2 } = req.body; + + const user = await userService.findByEmail(req.user.email); + const isPasswordValid = await bcrypt.compare(oldPassword, user.password); + + if (!isPasswordValid) { + return next( + ApiError.badRequest({ oldPassword: 'Old password is not correct' }), + ); + } + + if (newPassword1.trim() !== newPassword2.trim()) { + return next( + ApiError.badRequest({ newPassword: 'New passwords do not match' }), + ); + } + + if (newPassword1.trim() === oldPassword.trim()) { + return next( + ApiError.badRequest({ + newPassword: 'New password must be different from old password', + }), + ); + } + + const password = await bcrypt.hash(newPassword1, 10); + + user.password = password; + await user.save(); + + res.status(200).send('Password changed'); +}; + +const changeEmail = async (req, res) => { + const { password, newEmail } = req.body; + + const user = await userService.findByEmail(req.user.email); + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + return next( + ApiError.badRequest({ oldPassword: 'Password is not correct' }), + ); + } + + const confirmToken = uuidv4(); + + user.confirmToken = confirmToken; + user.confirmTokenExpiry = new Date(Date.now() + 1 * 60 * 60 * 1000); + user.pendingEmail = newEmail; + await user.save(); + + await emailService.sendConfirmEmail( + newEmail, + confirmToken, + 'Confirm changing Email', + 'Changing Email address', + ); + + res.status(200).send('Confirmation email sent'); +}; + +const confirmEmail = async (req, res) => { + const { confirmToken } = req.params; + + const user = await User.findOne({ where: { confirmToken } }); + + if (!user) { + throw ApiError.notFound(); + } + + if (user.confirmTokenExpiry < new Date()) { + user.confirmToken = null; + user.confirmTokenExpiry = null; + user.pendingEmail = null; + await user.save(); + + return next(ApiError.badRequest({ message: 'Token expired' })); + } + + const oldEmail = user.email; + + user.email = user.pendingEmail; + user.pendingEmail = null; + user.confirmToken = null; + user.confirmTokenExpiry = null; + await user.save(); + + await emailService.sendEmailChanged(oldEmail, user.email); + + res.status(200).send('Email changed'); +}; + +export const profileController = { + getData, + changePassword, + changeEmail, + confirmEmail, +}; diff --git a/src/exeptions/api.error.js b/src/exeptions/api.error.js new file mode 100644 index 00000000..20a081cd --- /dev/null +++ b/src/exeptions/api.error.js @@ -0,0 +1,32 @@ +export class ApiError extends Error { + constructor({ message, status, errors }) { + super(message); + + this.status = status; + this.errors = errors; + } + + static badRequest(message, errors) { + return new ApiError({ + message, + errors, + status: 400, + }); + } + + static unAuthorized(errors) { + return new ApiError({ + message: 'unauthorized user', + errors, + status: 401, + }); + } + + static notFound(errors) { + return new ApiError({ + message: 'not found', + errors, + status: 404, + }); + } +} diff --git a/src/index.js b/src/index.js index ad9a93a7..149987e5 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,35 @@ -'use strict'; +import 'dotenv/config'; +import express from 'express'; +import cors from 'cors'; +import cookieParser from 'cookie-parser'; +import { authRouter } from './routes/auth.route.js'; +import { errorMiddleware } from './middlewares/errorMiddleware.js'; +import { profileRouter } from './routes/profile.route.js'; + +const PORT = process.env.PORT || 3005; + +const app = express(); + +app.use(express.json()); +app.use(cookieParser()); + +app.use( + cors({ + origin: process.env.CLIENT_HOST, + credentials: true, + }), +); + +app.use(authRouter); +app.use('/profile', profileRouter) + +app.get('/', (req, res) => { + res.send('Ok'); +}); + +app.use(errorMiddleware); + +app.listen(PORT, () => { + // eslint-disable-next-line + console.log(`Server running on localhost:${PORT}`); +}); diff --git a/src/middlewares/authMiddleware.js b/src/middlewares/authMiddleware.js new file mode 100644 index 00000000..f7df1e8a --- /dev/null +++ b/src/middlewares/authMiddleware.js @@ -0,0 +1,20 @@ +import { ApiError } from '../exeptions/api.error.js'; +import { jwtService } from '../services/jwt.service.js'; + +export const authMiddleware = (req, res, next) => { + const authorization = req.headers['authorization'] || ''; + const [, token] = authorization.split(' '); + + if (!authorization || !token) { + throw ApiError.unAuthorized({}); + } + + const userData = jwtService.verify(token); + + if (!userData) { + throw ApiError.unAuthorized({}); + } + + req.user = userData; + next(); +}; diff --git a/src/middlewares/errorMiddleware.js b/src/middlewares/errorMiddleware.js new file mode 100644 index 00000000..bc47c9da --- /dev/null +++ b/src/middlewares/errorMiddleware.js @@ -0,0 +1,13 @@ +import { ApiError } from '../exeptions/api.error.js'; + +export const errorMiddleware = (error, req, res, next) => { + if (error instanceof ApiError) { + return res.status(error.status).send({ + message: error.message, + errors: error.errors, + }); + } + + res.statusCode = 500; + res.send({ message: 'Server error' }); +}; diff --git a/src/models/token.js b/src/models/token.js new file mode 100644 index 00000000..5e009918 --- /dev/null +++ b/src/models/token.js @@ -0,0 +1,13 @@ +import { DataTypes } from 'sequelize'; +import { client } from '../utils/db.js'; +import { User } from './user.js'; + +export const Token = client.define('token', { + refreshToken: { + type: DataTypes.STRING, + allowNull: false, + }, +}); + +Token.belongsTo(User); +User.hasOne(Token); diff --git a/src/models/user.js b/src/models/user.js new file mode 100644 index 00000000..506552e7 --- /dev/null +++ b/src/models/user.js @@ -0,0 +1,33 @@ +import { DataTypes } from 'sequelize'; +import { client } from '../utils/db.js'; + +export const User = client.define('user', { + name: { + type: DataTypes.STRING, + allowNull: false, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + password: { + type: DataTypes.STRING, + allowNull: false, + }, + activationToken: { + type: DataTypes.STRING, + }, + confirmToken: { + type: DataTypes.STRING, + defaultValue: null, + }, + pendingEmail: { + type: DataTypes.STRING, + defaultValue: null, + }, + confirmTokenExpiry: { + type: DataTypes.DATE, + defaultValue: null, + }, +}); diff --git a/src/routes/auth.route.js b/src/routes/auth.route.js new file mode 100644 index 00000000..a5e39e12 --- /dev/null +++ b/src/routes/auth.route.js @@ -0,0 +1,21 @@ +import express from 'express'; +import { authController } from '../controllers/auth.controller.js'; +import { catchError } from '../utils/catchError.js'; + +export const authRouter = new express.Router(); + +authRouter.post('/registration', catchError(authController.register)); + +authRouter.get( + '/activation/:activationToken', + catchError(authController.activate), +); + +authRouter.post('/login', catchError(authController.login)); +authRouter.get('/refresh', catchError(authController.refresh)); +authRouter.post('/logout', catchError(authController.logout)); +authRouter.post('/reset-password', catchError(authController.resetPassword)); +authRouter.post( + '/reset-password/:confirmToken', + catchError(authController.confirmResetPassword), +); diff --git a/src/routes/profile.route.js b/src/routes/profile.route.js new file mode 100644 index 00000000..e9a77f86 --- /dev/null +++ b/src/routes/profile.route.js @@ -0,0 +1,24 @@ +import express from 'express'; +import { authMiddleware } from '../middlewares/authMiddleware.js'; +import { profileController } from '../controllers/profile.controller.js'; +import { catchError } from '../utils/catchError.js'; + +export const profileRouter = new express.Router(); + +profileRouter.get('/', authMiddleware, catchError(profileController.getData)); + +profileRouter.post( + '/change-email', + authMiddleware, + catchError(profileController.changeEmail), +); +profileRouter.get( + '/change-email/:confirmToken', + catchError(profileController.confirmEmail), +); + +profileRouter.post( + '/change-password', + authMiddleware, + catchError(profileController.changePassword), +); diff --git a/src/services/email.service.js b/src/services/email.service.js new file mode 100644 index 00000000..1076eb09 --- /dev/null +++ b/src/services/email.service.js @@ -0,0 +1,85 @@ +import 'dotenv/config'; +import nodemailer from 'nodemailer'; + +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: process.env.SMTP_PORT, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD, + }, +}); + +export function send({ email, subject, html }) { + return transporter.sendMail({ + to: email, + subject, + html, + }); +} + +function sendActivationEmail(email, token) { + const href = `${process.env.CLIENT_HOST}/activate/${token}`; + + const html = ` +

Activate account

+ ${href} + `; + + return send({ + email, + html, + subject: 'Activate', + }); +} + +function sendConfirmEmail(email, token) { + const href = `${process.env.CLIENT_HOST}/change-email/${token}`; + + const html = ` +

Confirm changing Email

+ ${href} + `; + + return send({ + email, + html, + subject: 'Changing Email address', + }); +} + +function sendEmailChanged(oldEmail, newEmail) { + const html = ` +

Your email has been succesfull changed!

+

Email changed from ${oldEmail} to ${newEmail}

+ `; + + return send({ + email, + html, + subject: 'Email been changed', + }); +} + +function sendResetPassword(email, token) { + const href = `${process.env.CLIENT_HOST}/reset-password/${token}`; + + const html = ` +

Confirm password changing

+ ${href} + `; + + return send({ + email, + html, + subject: 'Password changing', + }); +} + +export const emailService = { + send, + sendActivationEmail, + sendConfirmEmail, + sendEmailChanged, + sendResetPassword, +}; diff --git a/src/services/jwt.service.js b/src/services/jwt.service.js new file mode 100644 index 00000000..5968f14f --- /dev/null +++ b/src/services/jwt.service.js @@ -0,0 +1,38 @@ +import jwt from 'jsonwebtoken'; + +function sign(user) { + const token = jwt.sign(user, process.env.JWT_KEY, { + expiresIn: '1h', + }); + + return token; +} + +function verify(token) { + try { + return jwt.verify(token, process.env.JWT_KEY); + } catch { + return null; + } +} + +function signRefresh(user) { + const token = jwt.sign(user, process.env.JWT_REFRESHKEY); + + return token; +} + +function verifyRefresh(token) { + try { + return jwt.verify(token, process.env.JWT_REFRESHKEY); + } catch { + return null; + } +} + +export const jwtService = { + sign, + verify, + signRefresh, + verifyRefresh, +}; diff --git a/src/services/token.service.js b/src/services/token.service.js new file mode 100644 index 00000000..b694b457 --- /dev/null +++ b/src/services/token.service.js @@ -0,0 +1,26 @@ +import { Token } from '../models/token.js'; + +async function save(userId, newToken) { + const token = await Token.findOne({ where: { userId } }); + + if (!token) { + await Token.create({ userId, refreshToken: newToken }); + } + + token.refreshToken = newToken; + await token.save(); +} + +function getByToken(refreshToken) { + return Token.findOne({ where: { refreshToken } }); +} + +function remove(userId) { + Token.destroy({ where: { userId } }); +} + +export const tokenService = { + save, + getByToken, + remove, +}; diff --git a/src/services/user.service.js b/src/services/user.service.js new file mode 100644 index 00000000..802db695 --- /dev/null +++ b/src/services/user.service.js @@ -0,0 +1,39 @@ +import { v4 as uuidv4 } from 'uuid'; +import { emailService } from './email.service.js'; +import { User } from '../models/user.js'; +import { ApiError } from '../exeptions/api.error.js'; + +function normalize({ id, email }) { + return { id, email }; +} + +function profileNormalize({ id, name, email }) { + return { id, name, email }; +} + +function findByEmail(email) { + return User.findOne({ where: { email } }); +} + +async function register(name, email, password) { + const activationToken = uuidv4(); + + const existUser = await findByEmail(email); + + if (existUser) { + throw ApiError.badRequest('User already exist', { + email: 'User already exist', + }); + } + + await User.create({ + name, + email, + password, + activationToken, + }); + + await emailService.sendActivationEmail(email, activationToken); +} + +export const userService = { normalize, profileNormalize, findByEmail, register }; diff --git a/src/utils/catchError.js b/src/utils/catchError.js new file mode 100644 index 00000000..0e1e7d8f --- /dev/null +++ b/src/utils/catchError.js @@ -0,0 +1,9 @@ +export const catchError = (action) => { + return async function (req, res, next) { + try { + await action(req, res, next); + } catch (error) { + next(error); + } + }; +}; diff --git a/src/utils/db.js b/src/utils/db.js new file mode 100644 index 00000000..fe4bf9c7 --- /dev/null +++ b/src/utils/db.js @@ -0,0 +1,9 @@ +import { Sequelize } from 'sequelize'; + +export const client = new Sequelize({ + host: process.env.DB_HOST, + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + dialect: 'postgres', +}); From c745de9f71e79fae69023634a3273886ffbf1c36 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Thu, 11 Jun 2026 17:41:58 +0300 Subject: [PATCH 02/19] fixed issues --- src/controllers/auth.controller.js | 9 ++++---- src/controllers/profile.controller.js | 30 ++++++++++++++++++--------- src/index.js | 7 ++++--- src/middlewares/authMiddleware.js | 3 ++- src/routes/profile.route.js | 6 ++++++ src/services/email.service.js | 2 +- src/services/token.service.js | 17 +++++---------- src/services/user.service.js | 4 ++-- src/utils/catchError.js | 2 +- 9 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 7108bed6..b71744db 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -1,4 +1,5 @@ -import bcrypt, { hash } from 'bcrypt'; +import bcrypt from 'bcrypt'; +import { v4 as uuidv4 } from 'uuid'; import { User } from '../models/user.js'; import { userService } from '../services/user.service.js'; import { jwtService } from '../services/jwt.service.js'; @@ -70,7 +71,7 @@ const activate = async (req, res) => { } user.activationToken = null; - user.save(); + await user.save(); res.send(user); }; @@ -79,9 +80,9 @@ const login = async (req, res) => { const { email, password } = req.body; const user = await userService.findByEmail(email); - const isPasswordValid = bcrypt.compare(password, user.password); + const isPasswordValid = await bcrypt.compare(password, user.password); - if (!email || !isPasswordValid) { + if (!user || !isPasswordValid) { throw ApiError.badRequest('Email or password is incorrect'); } diff --git a/src/controllers/profile.controller.js b/src/controllers/profile.controller.js index 998840dd..bd690dd7 100644 --- a/src/controllers/profile.controller.js +++ b/src/controllers/profile.controller.js @@ -1,9 +1,9 @@ import bcrypt from 'bcrypt'; import { v4 as uuidv4 } from 'uuid'; import { ApiError } from '../exeptions/api.error.js'; -import { jwtService } from '../services/jwt.service.js'; import { userService } from '../services/user.service.js'; import { emailService } from '../services/email.service.js'; +import { User } from '../models/user.js'; const getData = async (req, res) => { const user = await userService.findByEmail(req.user.email); @@ -11,7 +11,21 @@ const getData = async (req, res) => { res.status(200).send(userService.profileNormalize(user)); }; -const changePassword = async (req, res) => { +const changeName = async (req, res) => { + const { name } = req.body; + const user = await userService.findByEmail(req.user.email); + + if (!user) { + throw ApiError.unAuthorized(); + } + + user.name = name; + await user.save(); + + res.status(200).send(userService.profileNormalize(user)); +}; + +const changePassword = async (req, res, next) => { const { oldPassword, newPassword1, newPassword2 } = req.body; const user = await userService.findByEmail(req.user.email); @@ -45,7 +59,7 @@ const changePassword = async (req, res) => { res.status(200).send('Password changed'); }; -const changeEmail = async (req, res) => { +const changeEmail = async (req, res, next) => { const { password, newEmail } = req.body; const user = await userService.findByEmail(req.user.email); @@ -64,17 +78,12 @@ const changeEmail = async (req, res) => { user.pendingEmail = newEmail; await user.save(); - await emailService.sendConfirmEmail( - newEmail, - confirmToken, - 'Confirm changing Email', - 'Changing Email address', - ); + await emailService.sendConfirmEmail(newEmail, confirmToken); res.status(200).send('Confirmation email sent'); }; -const confirmEmail = async (req, res) => { +const confirmEmail = async (req, res, next) => { const { confirmToken } = req.params; const user = await User.findOne({ where: { confirmToken } }); @@ -107,6 +116,7 @@ const confirmEmail = async (req, res) => { export const profileController = { getData, + changeName, changePassword, changeEmail, confirmEmail, diff --git a/src/index.js b/src/index.js index 149987e5..a3742b62 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ import cookieParser from 'cookie-parser'; import { authRouter } from './routes/auth.route.js'; import { errorMiddleware } from './middlewares/errorMiddleware.js'; import { profileRouter } from './routes/profile.route.js'; +import { ApiError } from './exeptions/api.error.js'; const PORT = process.env.PORT || 3005; @@ -21,10 +22,10 @@ app.use( ); app.use(authRouter); -app.use('/profile', profileRouter) +app.use('/profile', profileRouter); -app.get('/', (req, res) => { - res.send('Ok'); +app.use((req, res, next) => { + next(ApiError.notFound()); }); app.use(errorMiddleware); diff --git a/src/middlewares/authMiddleware.js b/src/middlewares/authMiddleware.js index f7df1e8a..9807b132 100644 --- a/src/middlewares/authMiddleware.js +++ b/src/middlewares/authMiddleware.js @@ -16,5 +16,6 @@ export const authMiddleware = (req, res, next) => { } req.user = userData; - next(); + + return next(); }; diff --git a/src/routes/profile.route.js b/src/routes/profile.route.js index e9a77f86..0e8ac72c 100644 --- a/src/routes/profile.route.js +++ b/src/routes/profile.route.js @@ -7,6 +7,11 @@ export const profileRouter = new express.Router(); profileRouter.get('/', authMiddleware, catchError(profileController.getData)); +profileRouter.post( + '/change-name', + authMiddleware, + catchError(profileController.changeName), +); profileRouter.post( '/change-email', authMiddleware, @@ -14,6 +19,7 @@ profileRouter.post( ); profileRouter.get( '/change-email/:confirmToken', + authMiddleware, catchError(profileController.confirmEmail), ); diff --git a/src/services/email.service.js b/src/services/email.service.js index 1076eb09..8f920572 100644 --- a/src/services/email.service.js +++ b/src/services/email.service.js @@ -55,7 +55,7 @@ function sendEmailChanged(oldEmail, newEmail) { `; return send({ - email, + email: oldEmail, html, subject: 'Email been changed', }); diff --git a/src/services/token.service.js b/src/services/token.service.js index b694b457..84ec03b6 100644 --- a/src/services/token.service.js +++ b/src/services/token.service.js @@ -1,22 +1,15 @@ import { Token } from '../models/token.js'; async function save(userId, newToken) { - const token = await Token.findOne({ where: { userId } }); - - if (!token) { - await Token.create({ userId, refreshToken: newToken }); - } - - token.refreshToken = newToken; - await token.save(); + await Token.upsert({ userId, refreshToken: newToken }); } -function getByToken(refreshToken) { - return Token.findOne({ where: { refreshToken } }); +async function getByToken(refreshToken) { + return await Token.findOne({ where: { refreshToken } }); } -function remove(userId) { - Token.destroy({ where: { userId } }); +async function remove(userId) { + await Token.destroy({ where: { userId } }); } export const tokenService = { diff --git a/src/services/user.service.js b/src/services/user.service.js index 802db695..f3d8d729 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -11,8 +11,8 @@ function profileNormalize({ id, name, email }) { return { id, name, email }; } -function findByEmail(email) { - return User.findOne({ where: { email } }); +async function findByEmail(email) { + return await User.findOne({ where: { email } }); } async function register(name, email, password) { diff --git a/src/utils/catchError.js b/src/utils/catchError.js index 0e1e7d8f..5abe3209 100644 --- a/src/utils/catchError.js +++ b/src/utils/catchError.js @@ -3,7 +3,7 @@ export const catchError = (action) => { try { await action(req, res, next); } catch (error) { - next(error); + return next(error); } }; }; From f8c4314cf3864afa92940cba29aea6ff5f4265c4 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Thu, 11 Jun 2026 18:08:17 +0300 Subject: [PATCH 03/19] added activation check and redirection --- src/controllers/auth.controller.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index b71744db..1e550b54 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -73,13 +73,18 @@ const activate = async (req, res) => { user.activationToken = null; await user.save(); - res.send(user); + res.redirect(process.env.CLIENT_HOST + '/profile'); }; const login = async (req, res) => { const { email, password } = req.body; const user = await userService.findByEmail(email); + + if (user.activationToken) { + throw ApiError.badRequest('Please activate your email first'); + } + const isPasswordValid = await bcrypt.compare(password, user.password); if (!user || !isPasswordValid) { @@ -87,6 +92,7 @@ const login = async (req, res) => { } generateTokens(res, user); + res.redirect(process.env.CLIENT_HOST + '/profile'); }; const logout = async (req, res) => { @@ -100,7 +106,7 @@ const logout = async (req, res) => { await tokenService.remove(userData.id); - res.sendStatus(204); + res.sendStatus(204).redirect(process.env.CLIENT_HOST + '/login'); }; const refresh = async (req, res) => { From ec3ba914d84c00d019f4682d3750987a059f8ed9 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Thu, 11 Jun 2026 18:17:44 +0300 Subject: [PATCH 04/19] fixed problem with redirect and added middleware to logout --- src/controllers/auth.controller.js | 2 +- src/routes/auth.route.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 1e550b54..3a5f5c3d 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -106,7 +106,7 @@ const logout = async (req, res) => { await tokenService.remove(userData.id); - res.sendStatus(204).redirect(process.env.CLIENT_HOST + '/login'); + res.status(204).redirect(process.env.CLIENT_HOST + '/login'); }; const refresh = async (req, res) => { diff --git a/src/routes/auth.route.js b/src/routes/auth.route.js index a5e39e12..7f64f5b4 100644 --- a/src/routes/auth.route.js +++ b/src/routes/auth.route.js @@ -1,6 +1,7 @@ import express from 'express'; import { authController } from '../controllers/auth.controller.js'; import { catchError } from '../utils/catchError.js'; +import { authMiddleware } from '../middlewares/authMiddleware.js'; export const authRouter = new express.Router(); @@ -13,7 +14,7 @@ authRouter.get( authRouter.post('/login', catchError(authController.login)); authRouter.get('/refresh', catchError(authController.refresh)); -authRouter.post('/logout', catchError(authController.logout)); +authRouter.post('/logout', authMiddleware, catchError(authController.logout)); authRouter.post('/reset-password', catchError(authController.resetPassword)); authRouter.post( '/reset-password/:confirmToken', From dee0e3e318df8b57bf610c6f8fe30a23ccd3df39 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 15 Jun 2026 13:10:22 +0300 Subject: [PATCH 05/19] added passport.js with oauth --- src/controllers/auth.controller.js | 10 +++-- src/routes/oauth.route.js | 63 ++++++++++++++++++++++++++++++ src/services/email.service.js | 2 +- src/services/user.service.js | 19 ++++++++- src/utils/passport.js | 63 ++++++++++++++++++++++++++++++ 5 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 src/routes/oauth.route.js create mode 100644 src/utils/passport.js diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 3a5f5c3d..64c941d9 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -73,6 +73,8 @@ const activate = async (req, res) => { user.activationToken = null; await user.save(); + await generateTokens(res, user); + res.redirect(process.env.CLIENT_HOST + '/profile'); }; @@ -91,7 +93,8 @@ const login = async (req, res) => { throw ApiError.badRequest('Email or password is incorrect'); } - generateTokens(res, user); + await generateTokens(res, user); + res.redirect(process.env.CLIENT_HOST + '/profile'); }; @@ -106,7 +109,7 @@ const logout = async (req, res) => { await tokenService.remove(userData.id); - res.status(204).redirect(process.env.CLIENT_HOST + '/login'); + res.redirect(process.env.CLIENT_HOST + '/login'); }; const refresh = async (req, res) => { @@ -124,7 +127,7 @@ const refresh = async (req, res) => { generateTokens(res, user); }; -const generateTokens = async (res, user) => { +export const generateTokens = async (res, user) => { const normalizedUser = userService.normalize(user); const accessToken = jwtService.sign(normalizedUser); @@ -209,4 +212,5 @@ export const authController = { logout, resetPassword, confirmResetPassword, + generateTokens, }; diff --git a/src/routes/oauth.route.js b/src/routes/oauth.route.js new file mode 100644 index 00000000..d4fd381a --- /dev/null +++ b/src/routes/oauth.route.js @@ -0,0 +1,63 @@ +import express from 'express'; +import passport from '../utils/passport.js'; +import { generateTokens } from '../controllers/auth.controller.js'; +import { tokenService } from '../services/token.service.js'; +import { userService } from '../services/user.service.js'; +import { jwtService } from '../services/jwt.service.js'; + +export const oauthRouter = express.Router(); + +oauthRouter.get( + '/auth/google', + passport.authenticate('google', { + scope: ['profile', 'email'], + session: false, + }), +); + +oauthRouter.get( + '/auth/google/callback', + passport.authenticate('google', { + session: false, + failureRedirect: `${process.env.CLIENT_HOST}/login`, + }), + async (req, res) => { + const normalizedUser = userService.normalize(req.user); + const accessToken = jwtService.sign(normalizedUser); + const refreshToken = jwtService.signRefresh(normalizedUser); + + await tokenService.save(normalizedUser.id, refreshToken); + + res.cookie('refreshToken', refreshToken, { + maxAge: 30 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + res.redirect( + `${process.env.CLIENT_HOST}/oauth?accessToken=${accessToken}&user=${encodeURIComponent(JSON.stringify(normalizedUser))}`, + ); + }, +); + +oauthRouter.get( + '/auth/github', + passport.authenticate('github', { scope: ['user:email'], session: false }), +); + +oauthRouter.get('/auth/github/callback', + passport.authenticate('github', { session: false, failureRedirect: `${process.env.CLIENT_HOST}/login` }), + async (req, res) => { + const normalizedUser = userService.normalize(req.user); + const accessToken = jwtService.sign(normalizedUser); + const refreshToken = jwtService.signRefresh(normalizedUser); + + await tokenService.save(normalizedUser.id, refreshToken); + + res.cookie('refreshToken', refreshToken, { + maxAge: 30 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + res.redirect(`${process.env.CLIENT_HOST}/oauth?accessToken=${accessToken}&user=${encodeURIComponent(JSON.stringify(normalizedUser))}`); + } +); diff --git a/src/services/email.service.js b/src/services/email.service.js index 8f920572..7d70922b 100644 --- a/src/services/email.service.js +++ b/src/services/email.service.js @@ -19,7 +19,7 @@ export function send({ email, subject, html }) { } function sendActivationEmail(email, token) { - const href = `${process.env.CLIENT_HOST}/activate/${token}`; + const href = `${process.env.CLIENT_HOST}/activation/${token}`; const html = `

Activate account

diff --git a/src/services/user.service.js b/src/services/user.service.js index f3d8d729..1f23600f 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -11,7 +11,7 @@ function profileNormalize({ id, name, email }) { return { id, name, email }; } -async function findByEmail(email) { +async function findByEmail(email) { return await User.findOne({ where: { email } }); } @@ -36,4 +36,19 @@ async function register(name, email, password) { await emailService.sendActivationEmail(email, activationToken); } -export const userService = { normalize, profileNormalize, findByEmail, register }; +const registerOAuth = async (name, email) => { + return User.create({ + name, + email, + password: uuidv4(), + activationToken: null, + }); +}; + +export const userService = { + normalize, + profileNormalize, + findByEmail, + register, + registerOAuth, +}; diff --git a/src/utils/passport.js b/src/utils/passport.js new file mode 100644 index 00000000..9f4584b7 --- /dev/null +++ b/src/utils/passport.js @@ -0,0 +1,63 @@ +import passport from 'passport'; +import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; +import { Strategy as GitHubStrategy } from 'passport-github2'; +import { userService } from '../services/user.service.js'; + +passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: '/auth/google/callback', + }, + async (accessToken, refreshToken, profile, done) => { + try { + const email = profile.emails[0].value; + const name = profile.displayName; + + let user = await userService.findByEmail(email); + + if (!user) { + user = await userService.registerOAuth(name, email); + } + + done(null, user); + } catch (error) { + done(error); + } + }, + ), +); + +passport.use( + new GitHubStrategy( + { + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: '/auth/github/callback', + scope: ['user:email'], + }, + async (accessToken, refreshToken, profile, done) => { + try { + const email = profile.emails?.[0]?.value; + const name = profile.displayName || profile.username; + + if (!email) { + return done(new Error('No email provided from GitHub')); + } + + let user = await userService.findByEmail(email); + + if (!user) { + user = await userService.registerOAuth(name, email); + } + + done(null, user); + } catch (error) { + done(error); + } + }, + ), +); + +export default passport; From 3857d1be2c35521f872d5a1e0fec504d504cd261 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 15 Jun 2026 13:10:51 +0300 Subject: [PATCH 06/19] added passport.js --- package-lock.json | 195 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 ++ src/index.js | 8 +- 3 files changed, 206 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 54f9d5da..ae0f7d39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,13 @@ "cookie-parser": "^1.4.7", "cors": "^2.8.6", "express": "^5.2.1", + "express-session": "^1.19.0", "jsonwebtoken": "^9.0.3", "nodemailer": "^8.0.11", + "passport": "^0.7.0", + "passport-facebook": "^3.0.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", "pg": "^8.21.0", "pg-hstore": "^2.3.4", "sequelize": "^6.37.8", @@ -2677,6 +2682,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -4400,6 +4414,50 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", + "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", + "license": "MIT", + "dependencies": { + "cookie": "~0.7.2", + "cookie-signature": "~1.0.7", + "debug": "~2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "~5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7779,6 +7837,12 @@ "node": ">=8" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7888,6 +7952,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8044,6 +8117,87 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-facebook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-3.0.0.tgz", + "integrity": "sha512-K/qNzuFsFISYAyC1Nma4qgY/12V3RSLFdFVsPKXiKZt434wOvthFW1p7zKa1iQihQMRhaWorVE1o3Vi1o+ZgeQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-github2": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz", + "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8101,6 +8255,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pg": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", @@ -8506,6 +8665,15 @@ } ] }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -9689,6 +9857,24 @@ "node": ">=14.17" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -9786,6 +9972,15 @@ "punycode": "^2.1.0" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", diff --git a/package.json b/package.json index 9f436e08..56e71bbf 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,13 @@ "cookie-parser": "^1.4.7", "cors": "^2.8.6", "express": "^5.2.1", + "express-session": "^1.19.0", "jsonwebtoken": "^9.0.3", "nodemailer": "^8.0.11", + "passport": "^0.7.0", + "passport-facebook": "^3.0.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", "pg": "^8.21.0", "pg-hstore": "^2.3.4", "sequelize": "^6.37.8", diff --git a/src/index.js b/src/index.js index a3742b62..bfcd3233 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,8 @@ import { authRouter } from './routes/auth.route.js'; import { errorMiddleware } from './middlewares/errorMiddleware.js'; import { profileRouter } from './routes/profile.route.js'; import { ApiError } from './exeptions/api.error.js'; +import { oauthRouter } from './routes/oauth.route.js'; +import passport from './utils/passport.js'; const PORT = process.env.PORT || 3005; @@ -21,8 +23,10 @@ app.use( }), ); -app.use(authRouter); -app.use('/profile', profileRouter); +app.use(passport.initialize()); +app.use(oauthRouter); +app.use('/api', authRouter); +app.use('/api/profile', profileRouter); app.use((req, res, next) => { next(ApiError.notFound()); From ef4dc8ce5d54d4fcac312b084911483a2bcabdbb Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 15 Jun 2026 13:11:05 +0300 Subject: [PATCH 07/19] added frontend --- client/.gitignore | 24 + client/index.html | 13 + client/package-lock.json | 1596 +++++++++++++++++ client/package.json | 23 + client/public/favicon.png | Bin 0 -> 11207 bytes client/public/icons.svg | 24 + client/src/App.vue | 188 ++ client/src/api/auth.js | 20 + client/src/api/index.js | 40 + client/src/api/profile.js | 17 + client/src/main.js | 9 + client/src/pages/ActivationPage.vue | 41 + client/src/pages/ConfirmResetPasswordPage.vue | 78 + client/src/pages/LoginPage.vue | 171 ++ client/src/pages/OAuthPage.vue | 26 + client/src/pages/ProfilePage.vue | 340 ++++ client/src/pages/RegisterPage.vue | 151 ++ client/src/pages/ResetPasswordPage.vue | 60 + client/src/router/index.js | 63 + client/src/stores/auth.js | 87 + client/src/style.css | 296 +++ client/vite.config.js | 13 + 22 files changed, 3280 insertions(+) create mode 100644 client/.gitignore create mode 100644 client/index.html create mode 100644 client/package-lock.json create mode 100644 client/package.json create mode 100644 client/public/favicon.png create mode 100644 client/public/icons.svg create mode 100644 client/src/App.vue create mode 100644 client/src/api/auth.js create mode 100644 client/src/api/index.js create mode 100644 client/src/api/profile.js create mode 100644 client/src/main.js create mode 100644 client/src/pages/ActivationPage.vue create mode 100644 client/src/pages/ConfirmResetPasswordPage.vue create mode 100644 client/src/pages/LoginPage.vue create mode 100644 client/src/pages/OAuthPage.vue create mode 100644 client/src/pages/ProfilePage.vue create mode 100644 client/src/pages/RegisterPage.vue create mode 100644 client/src/pages/ResetPasswordPage.vue create mode 100644 client/src/router/index.js create mode 100644 client/src/stores/auth.js create mode 100644 client/src/style.css create mode 100644 client/vite.config.js diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/client/index.html b/client/index.html new file mode 100644 index 00000000..87f92eb3 --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + + Auth App + + +
+ + + diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 00000000..c48f45b1 --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,1596 @@ +{ + "name": "client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "client", + "version": "0.0.0", + "dependencies": { + "axios": "^1.17.0", + "lucide-vue-next": "^1.0.0", + "pinia": "^3.0.4", + "vue": "^3.5.34", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.6", + "vite": "^8.0.12" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz", + "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.35", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz", + "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz", + "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.35", + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz", + "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz", + "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz", + "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz", + "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.35", + "@vue/runtime-core": "3.5.35", + "@vue/shared": "3.5.35", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz", + "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35" + }, + "peerDependencies": { + "vue": "3.5.35" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz", + "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-vue-next": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-1.0.0.tgz", + "integrity": "sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg==", + "deprecated": "Package deprecated. Please use @lucide/vue instead.", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz", + "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-sfc": "3.5.35", + "@vue/runtime-dom": "3.5.35", + "@vue/server-renderer": "3.5.35", + "@vue/shared": "3.5.35" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 00000000..89d72c9a --- /dev/null +++ b/client/package.json @@ -0,0 +1,23 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "start": "vite build && vite" + }, + "dependencies": { + "axios": "^1.17.0", + "lucide-vue-next": "^1.0.0", + "pinia": "^3.0.4", + "vue": "^3.5.34", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.6", + "vite": "^8.0.12" + } +} diff --git a/client/public/favicon.png b/client/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..346690eca089b4b0bbd0e7e0de0d633652294fed GIT binary patch literal 11207 zcmeHtc|6qL|L>WxCZWZWtRIF+)=-)(gT$aL$*x4UWQ#!xUbGj@nBn*VgU!yO;H^?mip zqg7wx$3lZ_p54gv=e8d`C+!kpTe`C{d9#129=BTm^8<3@KOp3EQimAtzdqqt#7PbA7@vz!e{ z^DP&2*e)U>>J6Wr$Y%Yk@?0iMw>CkWYNa~?oBD?~?*1R_2tL=Qj z(m=M(Q=RO!GbGjn%jMYrh;gd5*#ka% z9}pzV0MO00^n;SIjFbZM!=%nq@*{G{veV)3D;ehqZBVMyP(wM2vB&7B)I%#}VKO-^ z1)NdebYmf<8-yK002R3YgJSW01FQA0#|8t3pL_^UndpaV@h;*^-T`^I;eZJzmY-#d zTiy1T;tL5$BNw7J(2&ViyaW}$2YB*V?uboOj5Vfj>kYFlprJezn30EskRe$}&|$`m z+Yq*5AHbPQ(w;8qD>L^IgFpX9QWqg-s7mFPSZn%Q;#UcpagZ8`bpm(z``^Y;&DbS- zrnL#3hKfB&x1Q3gxj&9$=aXf>hQBX|Gpb1{1G!!nIK@Nr(#xNT2~ZTD4&N;gX#F z5=I;?*yjyNPsTC)n(li*k1KSun=D+yb=;=d@T z?Ip!ileeEJqb@y4(1yduRf>NnlOHR#xcMP2)OpZ7D!rvbkv!le3~?t*b_)5>tSYv+ z8OO#ko%vqd0Z;Y3es=}tmB~X!zwYDctClo<&(z1jvnnEMtIL9|J*!Gch$pe8^$!3? zG|+?w+eHk{Z~hCj-E$@qCl14{94~S$2}!tNRjDmaTo4Oc<|5CuJe!6t8$2t=zx@h$ zV+)<~1U<-WnJ@Av{H+Y<@|O?*l1h2~I#%sCx(mYv(27vZD*6$m4maOENoEXA+$9M} z;cu3QbNlndsek14U$vuuVH^Q8k24r2Lc8~Fxo;fK@6JRTfiJ&TuBxxyXUI=LSN)~} z=i1T-6f++5BYP~m$Z1fQ6oxS;t9*kb6-A%W51%XMNiHidciJXimq@kQ1BT61ie2Pt zP$eZl?*w}g#OX;(V2uA3dBCvhXm+vWhcouUl4f9ln3)XB? zN0cf-H{6zb=EAd;*9mOq%?MXRQtU?z|6~@DaPEM|YP>!DA~Y8`lOLhFhR}3(M`G31 zAKzL}I9@=RDdM^TSvUv1!^wi3jTe&BlvOFdf_>Y@O}JZ0FMxch)UR=g#V8iv9`K3x zmgfF`41qlj3F?`RYzQ~@Be^eDmTv$LhOvkd+_+`a!+Mj@lGqam98!>kRJer=uKf*$ z58&w6E5moy@RNY;O$Us&4PBdf+|>`Ubi;#~`Et6^ijpX2=#0Qtc4{caQ*{+Yiwm*u z(E#oCk0&-!!a=l~AF1E2R25Qjf$1%Yj3QRK`8_?Vq7oDSkw`DeOC8(ab;5qNaC2;< z1Xb%{Z_cz?E8LW+_Ro30PA@q^+_i%(Rnw2bt*kt{*oqfvyZ;;myr%AXEl2ga;)JbY zOa{+)IneK3RX=L`d8316?jSjd|ZW3BV)>iDRS=y5Q=GR^x{zC(rJhl#a zmOxe5r79KdWVj$(jR&u7Ws{Sj*$jg&ej`4`BN58DRTmaDy% zLz+hvzbJl^pq&k?EMIoRja1PKDnvySn#mj1iPyjEF`HfxhQ8wgS+js?sJwjkWe%l@ zyVRUMAiD9POS$CL&%M*NwGNn}KbWzpFR6SzYYLL*a>$pVKbEW=6;W~oOD8+`D{aM$ z%I`nGJPL<1@V8?2i{3tp`cz=)CzQ={!R^IWoJjl~)L0(=YXA~EQ3oc1prx-9<50CP zFlp88dkosXw8P9ah)KT=)nLY{CG|7;ro&^U&+I1G7RgpFrkkA{Bc$YSIZx( z0OMDKd4O2I$NQ9;^u3v)%}3C-*G0h`Dz9H(iOD@HC3D$xpemY|9+ z6hBwUf{AB0#gwYnu^n_N=k=3&{yux4P%+k>&a~eAa$v))xPE3lI0v=`MYfpMgu@SB z-)Lq3i3c|4QL%U29^5_EekRrwI_M)#f~J8R)7y#5Fn z2VG$pd!|`|Y;xDGocPK+HzQPXx{B#t8?~vO+jH;Z29IP>8;~Mxu%}6e0eIZmFXg7W zVl2V@cY7HN{kg;{rWKzr!6fMzmAtBdAcQ;$T_fCU`F>A4+V!X3a?~;AV?Fc1sKn)T z+lU1P&pN=Zn%CcK^+7Hm9iM;eR|5GJR22a)GN%U9`hl44N#(kxo}NhDsmq7W+S<0u zT@+7!(dBORdb+WsSty|_FZI3hDW-K{IgNj+IKxhd_oKfw_n|g!k%v}wTs=MWMD-S- zghr3MGe^5SwRpMHtv~`ywu{7O*SXDc(sQEyZ}*3A#TQt0wwVn)G5Ul_o8m?F!F+Vz zna_9%N?M^yXbeb0{_&g~;s zeF=YmW>(H$$`L#ZzjJFnsqq-yhaqo!n6#gr+0cqh?d9dZh7^&BLp*s>Of)PS@Ok7o zS4)ptJ7+eo`?L3*h=c$ZYtIx^i}!c>67CYbcg3A~wDTngB?LT$6&$;*_#5{+H`6`Q^jQ3kkSoz=l% zx-m(Koy7w_B$p(tcV6d8h}(4No4u6{NJJ^P=AA%(!H}0)X(Wd@ClLxl&=}qawDV@( zJV-2ppH{#?NO3wmAM@x^%kd3S#u2F==biilSiSy6p^00gP!}EydCz+hf**a0yS36q z6G{O$jO_I6+~)qz*>Sk|0BL0ZFDo+|_a86lw-5t%;8|!LISf*~WCbk|ER76COK)#= zo(}y>&&-=w-yFfuWfgNp(6YT3!(9}fQSQyPvo{^sf@jpKI|jk9_0@oEx>?e=uONPU zzUBu!)bPsItzt-Bew|IT)jRZ4kD?Pv_1*)v)N*xtG1n9pUQ9RIz(s|Yv|a-!fY$NB zcZe)IuuiidlNoOo0@GpXb5GxWJ|aNa(o0b|u>9!aQw;Ypc4PJugy<={aNFU|72SAbwbH5$Yf`bkrFX2F&zk0CE2 zTe?k!lm~o%EKYmTj`m^q?Q0-+<%d-{{5Dd!-AL@?c|Jvss3@q z(!P0v_SG%?tVlA6uGOuLkTC7wj4I!cD1o%ZXhPn}BWQ33#!wJU8q=XvQwBoBP3kCp zj_!;UhQi^K$#2G)PvlEpb^1#qzdK+e4>?b39x@sH*X`$DrQ@ES`=xh7!m7gzu_QjS zK<1-x$^HD#0HyfpzHXXUkMpFO0HMnyWd8mV%Sh0PDG~D=lhp8rwkre@5oPxlPuFo# z_m#<7m1UvE%E7O|4m|LF^}mco!vIX*)e2w=03p}nR5Q>S`M)ELOh68W_hcyw4Ph-0Y=y38gFa{?)cx=ZMEF^uzll}KKPH#Hh+d_Eg6)#L}r54W#4)A+UJu-?ISV}gcaPhK^={HZ2A0h~0~`WNc2S;4zNJ?)c5*Rx1};qC-0B@Z)5M*p z2}W_z-`^Ez1l{qMuJ{_WR@l;X-|YFAF_9lPXiH zXf7Tq{(6r)&gsDIU+kS$^>UhB)tvL2A;`4civ55)7pKRilPaZ|7J;+3Y%*&4SKTv7+D2dnpGyoa) z7DY@b#X{sllrio(SraAiCXPNqttXvkVNo z%7@(|pT$WAO7j5yhST&HmSSg{g(i0kVAVu-?S{{;OwU;~m?$7A7T25)OT&wteQ?af zZG0!>Vz~QhYOnJ!{{hefg}gg+&gcZg4p2`erpoMM)!;>BZNY`IZ2y}qGFgFA0SMD~ zaw6)8&kbkg!e}VMhc~F{=&y(vj$P&2QGL*S?=cod4M@$a zPAM;Bfn=@e;JdVQo;;p<0x1ndM=Qw?$TMpBxtg?r-Y+t&d;7)pK`6YZd^)VNE5c1S^oI zqBvIR|63MJWEMajHVExht^*DWs+=iLG!Ho?BW;fe9R#7m-r?8LAa6>J3#|TYIK{*( zlgw4;4ZzP!*nsro1@QI}1_r?Ay!1iQQsCfw2M+)>TcY9sWK@t(9JHlrwIlZsUWSGu z2#^2E@Fme*IS}8W0U%iWM8~m0a1UV_?+m4z+wF~qn}69CC=I{^oAkFn?IAb^FM@j zm~R&%DNMeB&#y(Yn*Gf~?nVB5c){G}9-H^JZWYDJ`$LRQJ7qOvOF#Bv ztMADf`fHd)F_DG03wbF=iwqwZgs`yA`+KL_b>q8EU}AcCr43>el`_X%5{H4R9SjJ`T-IEBZOpGqmzbD&9m@RO@im*6 zHFN*B`QKezd^#3fE+s26J*FN(3Qq@iv8Z%S;YXJRLG3H9Y<&&rd%f8!h0`j+Eeb9g~#miUN{Oe0W`xj<+ z%f%_hS-mUop#Z^`*`c47S)Xrf`!pFw_v|Xxo%Z>;17t!0Gr#TgXK=mpoBdjy`Q~=gi>o}LeITg&&R@U!77l!{w3vi@`<)iX7x z-FjymPsxlV`yS#)W*kQV$+{~e79Qc_%$Q5_IbHme(J~up^Y(0s`;vFL$GuFWttg@ouxLA2-`v zN8gqm2#M{ATX`8Vi^giA0hVihuArNhcV*yB|Cr+}y8px2(A}=h`%Mv}E9Vx{fMf;| z48K>s4PRw%* z?}RQqMo=zn&7zYQ4Ng5~A9prSDeg@0jn&->4*FmStHXpEoWTe0%!Di$oGNThS|OCU zjy8JdZhjrp9v-quKz@Tj`0&$~tfKPDeabGh;zre`q$=a!-v6cg}j8%kWhC-_x&upY1r7LdtQ96 zWx17=kxzX9BlVnfSBpoKMuE4qf?jKvX4Z}bswQXn7xR?)-T2ZMnB$?myoC`fL*}s1 zc|i*}Fk6DWBgPLNBD*|_e{bAz07!67k}3;JFeb5FMJA3g#auR#(-cfUgB8U@DMbv> z6Di)@iwom2<@J*mvl3WNf>7b8DCV+N@gt8|wHU%HP3&nXn&|SYUq2R8*Dh;{*owkU z%km19{l9$O#?Z2D^cp%wiHj}53(7&H9((=W??o9IVL=Dj2{_DrCi%LVQ{CX6heFPA zt8fmL9-)q>x=*| zVbk+cQ}Z1e?;*|5dLTUeFw)=i!3zwp$ac7k;OLbP!-uKN$@a0$oiVZI&p6}h{@A?_ zS3}p%x`*CVvXy~+-k3KCV(hf7@;aM_=av(Ldf6NK0P;}>z!xZ~&%`e5)wwwz9)RY9 zU#@*%%3GWS2#uO|5kCGQvkjSqgBcg*Tc8%;(FEFb%|Za{o_=Dk@!tb{JaUKNrk6Pz ze=t8-+w)uxQWfJ+9t?VNAy~3GGa=SLK*m!T*eY>Gm2lqg_-FLykvxhP6pI3R(*-4!=XU9-irw=z5Fcy%9Rma}r9?l!@r!g|-B1;N$Zj@udf$ z8j&l`UXfxWTL~{=GzkOF0EN(VA4kdghNSZ5I8VXgGdGm2|C_{2q0(~Ta zA=$E0u2kv;Nee6%tTf`eb2~_1;!9OH1Ka&5pZmgvBuMWnrygl@)o|#WSgjD$^}K5F z=@Z3K?+*U-2B%@Lo^B6)nkHH3DN41Z{pHak7$myMH&fHnFe}1%OCRF_?=!W{daqhfT7oBDp2m_oXwOaS#I1BbH zoJbZNTKjtc4Xfc3L+ekia1+wl(M1#t&pA%`UkH$SQ=)eDCFZ#1=IS%$w;y%PzdWpm=+ zTh`#~W$7dHr)MR`^n81HJtA~$*10Zl>VWg9p;E(mM#a51uXF*}uE%*7KwqwgS1IFt z?LhI5?Yb^}2z{6czIj@%MaIr{tm&xRHD~T}o7!de&%ZDoGl3dDNJCj$If7X@ZBY92 zq0B70VQ7p|(PglO=qq~TBh9k}PcUmLOqD_gb*Bxf3qJ&)FhlUNG*LOIArl$R;pBbU zS_s`~d{tziUc+=WIWG1`tnjxt8;4!Y5gjndB`0c_`&gC33KuH51#9ugx;k@Xj+vjB z77{4k3DuKWq!&)FXk`(ddl@^sJ}7St{S$rpK#=WghilQXd)VU9Iih;9CuNh7KliOH$w| zjY#>N9Qo~N+Hyl2y1#B)F?((M!ntKWDHteHj7Qq%q{}W8Mx}q%?Id)=j;w;yAO;6M z#;4y6f*lv-POv}sRVzazn0b@NQE^m+MC{3=EPW58E_z z%6#h9-!I@Tb@)ZgD%r?d3QipO=Q@8N7Oh3N&mnU4mj1})rM=*+9qjCO2ptNDO%FkQ z405w6Y+XDACSkSG`zy%}M0vvp7??39N?T;yx5hJG0YV{Mo_Muwv#ZdiglCDPiENv$ z+p(^F;3|CWL+bA#e0a%A!HL~)kz7fBU#yBY!3HkzE_VYP`@!fH*SDwB7Xvd89Lltg zhe7FV-gS&_tm`1S`gsJ+6-}kRpwkApd*H)d=xf)k{H=d@fK_BdLRR?|UM62sZh^Dk zRd@B(@7jnk=(et6tOD zf8#&b&8jkVKOT1&6;dK}0RZR1`I?7dU3Yp+bEf%A1oLozgkF{&G!rcRRgC({#PvIH z7iH9N>7jd2-$jhHC*bQ+o~qYK;EAX(7ZN64;4QD7)Pw}@IPJbkk)Y}NYeFDoz5(yn z?S|n0uFX{sYx@m0-?QlL6K)W_8qAljfCa)B&cAVHKGFekR`$L9{261?6a*k23?u=n*u|Bci9dzq_^fCLf(za!hSjgC$r zIpf1e0eQxe3fnZ;KY2p-Od$$Zr*KQ)5kN2?TV+~7PZ&F2ThG6%8brj$Q6-`LI#p^P zMG2SJf?y}X4RfS~tLDb(S$^NIi>>cr<=27x&UBcP0@iEFA*b7`uDitnacD(T1vfT+ z&`!gZ=HIp=>{fN;6zr5>y9m7FLx0(yka{wNIo@q8J3_ItiQ}|H`W)2_(yJHM+nt7b zgPSt#qxw!HvOgAd*_KxhU<5c5CJ4-_cKj!vtxA~28#3(=^QDj9?b@OmU?Sopx46_{ z3Cg^sdABA|{L{Vdna<^5DIV|GQ*Breshikz z9Q0V#D4|r3oIJH!e`tqb?^Nw&$iB`MuWF$?9cFwsF&^e8ubgO7ig2YNm&Bm%jn(?N zT=fDo#QI&mrFjU|ZQTF&Nb;9`@ZbZyQ~nov(E5o3UzAAPdvy$fKHJKHFATLDNhwHM zLm?FqD*G=SVyYAgR{!hpsA}y1h8v`95?2V`5jL82#3}^#Kve67;vqYL zo!X%ynP`U+poS`@X;`+y9usiSFz6ELDt6$;q9jC`MC5wM&Xq$ft??H*T2ln*(EBPI z#JeGElrdoO?V%6?AZ8qOzIxq4gO+Sncq|Y5Dj-78$m?|%gwjWD8q>8QH&pKdv}B+i z*B*i^PHYY3p;9{namwf#CwgUNgP1PduJZhMEgZ>(27s`KqolW;0-S0kwsnI313p?( z6-G$Cj>956rG|zIOv70bFp;p4-AYNrfAQY*2=JeCWGd4&?S zjN%#v0jz>z+6az)lrzQ^?h=XVi#k!&()gWAB_7s6dbt+U8BXqS$s7z;ESWH?$B zsW6RD^M0`KNPWLe7T)i2br@q|fd*m%DFwt}QbaCXr1>+w{L^Mvlh7{7pTId;h;<6} z=>l9K_(_H7pcD_EtPZ@GgfBW-vfDYcm+(08{9Y4@iCu*40x4;z4O_ZD#7}Mgj>2~j zUQG5uxR@qD*j~Q-Ge0g0;>!d~yD5FA^=7aq4^{Vi&+pVRCB0Fo>*2p%5$hcqqoHc% zj1i%B@;9?tM{x1-0YzbwpvI9z?jo_&P-$kfn!jTA0Fvk~h!0uX?Ywoqh!?zUaT)B{ z%CZ!@EPuNK4Z>_K(r>c1;Y#u&2yn5)OGzBm6XNmOCv!w{j!C%-d|y-jCQBaYeUS@6 zcz7VDotdR*0Ks0Wp76_Kh~#_zrzcEP@1T}~aZ^ED2!4xdvbo|$5_#+IkvM%ja8-mN zC{^K)pRmSNC-)_S3)NP14U~Z*(->#--O6Ya4LVg6t1NG81e{?GNa1!X;&V6C)i(MH zw + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/App.vue b/client/src/App.vue new file mode 100644 index 00000000..0a16b0a4 --- /dev/null +++ b/client/src/App.vue @@ -0,0 +1,188 @@ + + + diff --git a/client/src/api/auth.js b/client/src/api/auth.js new file mode 100644 index 00000000..d8aab162 --- /dev/null +++ b/client/src/api/auth.js @@ -0,0 +1,20 @@ +import api from './index.js'; + +export const authApi = { + register: (name, email, password) => + api.post('/api/registration', { name, email, password }), + + login: (email, password) => api.post('/api/login', { email, password }), + + logout: () => api.post('/api/logout'), + + refresh: () => api.get('/api/refresh'), + + resetPassword: (email) => api.post('/api/reset-password', { email }), + + confirmResetPassword: (confirmToken, newPassword1, newPassword2) => + api.post(`/api/reset-password/${confirmToken}`, { + newPassword1, + newPassword2, + }), +}; diff --git a/client/src/api/index.js b/client/src/api/index.js new file mode 100644 index 00000000..7b3b2903 --- /dev/null +++ b/client/src/api/index.js @@ -0,0 +1,40 @@ +import axios from 'axios'; +import { useAuthStore } from '../stores/auth.js'; + +const api = axios.create({ + withCredentials: true, +}); + +api.interceptors.request.use((config) => { + const auth = useAuthStore(); + if (auth.accessToken) { + config.headers.Authorization = `Bearer ${auth.accessToken}`; + } + return config; +}); + +api.interceptors.response.use( + (res) => res, + async (error) => { + const auth = useAuthStore(); + const original = error.config; + + if ( + error.response?.status === 401 && + !original._retry && + !original.url.includes('/refresh') + ) { + original._retry = true; + try { + await auth.refresh(); + original.headers.Authorization = `Bearer ${auth.accessToken}`; + return api(original); + } catch { + auth.logout(); + } + } + return Promise.reject(error); + }, +); + +export default api; diff --git a/client/src/api/profile.js b/client/src/api/profile.js new file mode 100644 index 00000000..e57ca151 --- /dev/null +++ b/client/src/api/profile.js @@ -0,0 +1,17 @@ +import api from './index.js'; + +export const profileApi = { + getProfile: () => api.get('/api/profile'), + + changeName: (name) => api.post('/api/profile/change-name', { name }), + + changePassword: (oldPassword, newPassword1, newPassword2) => + api.post('/api/profile/change-password', { + oldPassword, + newPassword1, + newPassword2, + }), + + changeEmail: (password, newEmail) => + api.post('/api/profile/change-email', { password, newEmail }), +}; diff --git a/client/src/main.js b/client/src/main.js new file mode 100644 index 00000000..f5e32ec4 --- /dev/null +++ b/client/src/main.js @@ -0,0 +1,9 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import App from './App.vue'; +import router from './router/index.js'; + +const app = createApp(App); +app.use(createPinia()); +app.use(router); +app.mount('#app'); diff --git a/client/src/pages/ActivationPage.vue b/client/src/pages/ActivationPage.vue new file mode 100644 index 00000000..cb617e67 --- /dev/null +++ b/client/src/pages/ActivationPage.vue @@ -0,0 +1,41 @@ + + + diff --git a/client/src/pages/ConfirmResetPasswordPage.vue b/client/src/pages/ConfirmResetPasswordPage.vue new file mode 100644 index 00000000..e210cf5b --- /dev/null +++ b/client/src/pages/ConfirmResetPasswordPage.vue @@ -0,0 +1,78 @@ + + + diff --git a/client/src/pages/LoginPage.vue b/client/src/pages/LoginPage.vue new file mode 100644 index 00000000..8aec8c76 --- /dev/null +++ b/client/src/pages/LoginPage.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/client/src/pages/OAuthPage.vue b/client/src/pages/OAuthPage.vue new file mode 100644 index 00000000..49aee118 --- /dev/null +++ b/client/src/pages/OAuthPage.vue @@ -0,0 +1,26 @@ + + + diff --git a/client/src/pages/ProfilePage.vue b/client/src/pages/ProfilePage.vue new file mode 100644 index 00000000..40dab7d6 --- /dev/null +++ b/client/src/pages/ProfilePage.vue @@ -0,0 +1,340 @@ + + + + + diff --git a/client/src/pages/RegisterPage.vue b/client/src/pages/RegisterPage.vue new file mode 100644 index 00000000..3e2d6754 --- /dev/null +++ b/client/src/pages/RegisterPage.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/client/src/pages/ResetPasswordPage.vue b/client/src/pages/ResetPasswordPage.vue new file mode 100644 index 00000000..17d1cd90 --- /dev/null +++ b/client/src/pages/ResetPasswordPage.vue @@ -0,0 +1,60 @@ + + + diff --git a/client/src/router/index.js b/client/src/router/index.js new file mode 100644 index 00000000..e33bd1dc --- /dev/null +++ b/client/src/router/index.js @@ -0,0 +1,63 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import { useAuthStore } from '../stores/auth.js'; + +const routes = [ + { path: '/', redirect: '/profile' }, + { + path: '/login', + component: () => import('../pages/LoginPage.vue'), + meta: { guest: true }, + }, + { + path: '/register', + component: () => import('../pages/RegisterPage.vue'), + meta: { guest: true }, + }, + { + path: '/oauth', + component: () => import('../pages/OAuthPage.vue'), + meta: { public: true }, + }, + { + path: '/reset-password', + component: () => import('../pages/ResetPasswordPage.vue'), + meta: { guest: true }, + }, + { + path: '/reset-password/:confirmToken', + component: () => import('../pages/ConfirmResetPasswordPage.vue'), + meta: { guest: true }, + }, + { + path: '/profile', + component: () => import('../pages/ProfilePage.vue'), + meta: { auth: true }, + }, + { + path: '/activation/:activationToken', + component: () => import('../pages/ActivationPage.vue'), + meta: { public: true }, + }, +]; + +const router = createRouter({ + history: createWebHistory(), + routes, +}); + +router.beforeEach(async (to) => { + const auth = useAuthStore(); + + if (to.meta.public) return; + + if (!auth.user && !auth.accessToken) { + try { + await auth.refresh(); + } catch {} + } + + if (to.meta.auth && !auth.user) return '/login'; + if (to.meta.guest && auth.user) return '/profile'; +}); + +export default router; diff --git a/client/src/stores/auth.js b/client/src/stores/auth.js new file mode 100644 index 00000000..b1a9c953 --- /dev/null +++ b/client/src/stores/auth.js @@ -0,0 +1,87 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { authApi } from '../api/auth.js'; +import router from '../router/index.js'; + +export const useAuthStore = defineStore('auth', () => { + const user = ref(null); + const accessToken = ref(null); + const loading = ref(false); + const error = ref(null); + + const setAuth = (userData, token) => { + user.value = userData; + accessToken.value = token; + }; + + const clearAuth = () => { + user.value = null; + accessToken.value = null; + }; + + const register = async (name, email, password) => { + loading.value = true; + error.value = null; + try { + await authApi.register(name, email, password); + return true; + } catch (err) { + error.value = err.response?.data?.errors || { + general: 'Registration failed', + }; + return false; + } finally { + loading.value = false; + } + }; + + const login = async (email, password) => { + loading.value = true; + error.value = null; + try { + const res = await authApi.login(email, password); + setAuth(res.data.user, res.data.accessToken); + router.push('/profile'); + return true; + } catch (err) { + error.value = err.response?.data?.errors || { + general: err.response?.data?.message || 'Login failed', + }; + return false; + } finally { + loading.value = false; + } + }; + + const logout = async () => { + try { + await authApi.logout(); + } finally { + clearAuth(); + router.push('/login'); + } + }; + + const refresh = async () => { + try { + const res = await authApi.refresh(); + setAuth(res.data.user, res.data.accessToken); + } catch { + clearAuth(); + throw new Error('Refresh failed'); + } + }; + + return { + user, + accessToken, + loading, + error, + register, + login, + logout, + refresh, + setAuth, + clearAuth, + }; +}); diff --git a/client/src/style.css b/client/src/style.css new file mode 100644 index 00000000..527d4fba --- /dev/null +++ b/client/src/style.css @@ -0,0 +1,296 @@ +:root { + --text: #6b6375; + --text-h: #08060d; + --bg: #fff; + --border: #e5e4e7; + --code-bg: #f4f3ec; + --accent: #aa3bff; + --accent-bg: rgba(170, 59, 255, 0.1); + --accent-border: rgba(170, 59, 255, 0.5); + --social-bg: rgba(244, 243, 236, 0.5); + --shadow: + rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + + --sans: system-ui, 'Segoe UI', Roboto, sans-serif; + --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + --mono: ui-monospace, Consolas, monospace; + + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light dark; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + @media (max-width: 1024px) { + font-size: 16px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #9ca3af; + --text-h: #f3f4f6; + --bg: #16171d; + --border: #2e303a; + --code-bg: #1f2028; + --accent: #c084fc; + --accent-bg: rgba(192, 132, 252, 0.15); + --accent-border: rgba(192, 132, 252, 0.5); + --social-bg: rgba(47, 48, 58, 0.5); + --shadow: + rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; + } + + #social .button-icon { + filter: invert(1) brightness(2); + } +} + +body { + margin: 0; +} + +h1, +h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); +} + +h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } +} +h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: 20px; + } +} +p { + margin: 0; +} + +code, +.counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); +} + +code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); +} + +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#app { + width: 1126px; + max-width: 100%; + margin: 0 auto; + text-align: center; + border-inline: 1px solid var(--border); + min-height: 100svh; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 00000000..670bb3fa --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], + server: { + port: 3000, + proxy: { + '/api': { target: 'http://localhost:3005' }, + }, + }, + appType: 'spa', +}); From 5a95116412756dae2f458c0bd321b1ece8468f51 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 15 Jun 2026 13:31:03 +0300 Subject: [PATCH 08/19] rename for commonJS project --- .eslintrc.js => .eslintrc.cjs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .eslintrc.js => .eslintrc.cjs (100%) diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 100% rename from .eslintrc.js rename to .eslintrc.cjs From 6bcc3e157c63cb1e5fb54ee50e24a23c52185279 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 15 Jun 2026 13:44:38 +0300 Subject: [PATCH 09/19] trigger CI From c7f116d26fb5a525893d93fb5d20326a42300ce7 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 15 Jun 2026 13:48:18 +0300 Subject: [PATCH 10/19] added eslintConfig --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 56e71bbf..f44dd58f 100644 --- a/package.json +++ b/package.json @@ -47,5 +47,8 @@ "pg-hstore": "^2.3.4", "sequelize": "^6.37.8", "uuid": "^14.0.0" + }, + "eslintConfig": { + "root": true } } From f32ce91b6222aa8226cff5d7c261afc9f7b7d27c Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 15 Jun 2026 13:51:41 +0300 Subject: [PATCH 11/19] deleted type=module --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index f44dd58f..1cd1c3a5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "node_auth-app", "version": "1.0.0", "description": "Auth app", - "type": "module", "main": "src/index.js", "scripts": { "init": "mate-scripts init", From e70b248ceffad442592c392b0e3e4ce68c96490d Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 15 Jun 2026 14:49:50 +0300 Subject: [PATCH 12/19] returned type module --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1cd1c3a5..f44dd58f 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "node_auth-app", "version": "1.0.0", "description": "Auth app", + "type": "module", "main": "src/index.js", "scripts": { "init": "mate-scripts init", From 6e30f054f2a4fa2fbff51a5fecb30f58bb1f8bf3 Mon Sep 17 00:00:00 2001 From: Yaroslav Hohulia Date: Mon, 15 Jun 2026 14:54:24 +0300 Subject: [PATCH 13/19] Delete .eslintrc.cjs --- .eslintrc.cjs | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 .eslintrc.cjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index f44c7a1d..00000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - extends: '@mate-academy/eslint-config', - env: { - jest: true - }, - rules: { - 'no-proto': 0 - }, - plugins: ['jest'] -}; From 83004c61374ff5830fecb05ec83d1dae57f8f83d Mon Sep 17 00:00:00 2001 From: Yaroslav Hohulia Date: Mon, 15 Jun 2026 15:00:28 +0300 Subject: [PATCH 14/19] fix: rename .eslintrc.js to .eslintrc.cjs for ES module compatibility --- .eslintrc.cjs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .eslintrc.cjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000..f44c7a1d --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,10 @@ +module.exports = { + extends: '@mate-academy/eslint-config', + env: { + jest: true + }, + rules: { + 'no-proto': 0 + }, + plugins: ['jest'] +}; From c1266cb66065f83028ba94bf999631b620663456 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 15 Jun 2026 15:07:23 +0300 Subject: [PATCH 15/19] trigger CI From c7c7ed16aff26c54778cfa0afd0f87cbff2259c0 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 15 Jun 2026 15:11:35 +0300 Subject: [PATCH 16/19] changes uses --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bb13dfc4..db1cd5bd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: node-version: [20.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: From 13ed7d2be7cd5d0b7ae969d969343bc1baf8aacc Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Thu, 18 Jun 2026 16:07:30 +0300 Subject: [PATCH 17/19] added confirm page --- client/src/pages/ConfirmChangeEmailPage.vue | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 client/src/pages/ConfirmChangeEmailPage.vue diff --git a/client/src/pages/ConfirmChangeEmailPage.vue b/client/src/pages/ConfirmChangeEmailPage.vue new file mode 100644 index 00000000..90de328b --- /dev/null +++ b/client/src/pages/ConfirmChangeEmailPage.vue @@ -0,0 +1,45 @@ + + + From 59936e21fbf3d4550205a4a9a3c3d23ef23735b5 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Thu, 18 Jun 2026 16:08:14 +0300 Subject: [PATCH 18/19] added error handling --- src/controllers/auth.controller.js | 53 +++++++++++++++++++-------- src/controllers/profile.controller.js | 47 ++++++++++++++++-------- src/index.js | 2 +- src/middlewares/errorMiddleware.js | 2 +- src/routes/auth.route.js | 1 + src/routes/oauth.route.js | 24 +++++++++--- src/routes/profile.route.js | 2 + src/services/user.service.js | 4 +- 8 files changed, 95 insertions(+), 40 deletions(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 64c941d9..2e1cc1f0 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -78,19 +78,36 @@ const activate = async (req, res) => { res.redirect(process.env.CLIENT_HOST + '/profile'); }; -const login = async (req, res) => { +const login = async (req, res, next) => { const { email, password } = req.body; + const errors = { + email: validateEmail(email), + password: validatePassword(password), + }; + + if (errors.email || errors.password) { + throw ApiError.badRequest('', errors); + } + + if (!email || !password) { + return next(ApiError.badRequest('All fields are required')); + } + const user = await userService.findByEmail(email); - if (user.activationToken) { - throw ApiError.badRequest('Please activate your email first'); + if (!user) { + return next(ApiError.badRequest('Email or password is incorrect')); } const isPasswordValid = await bcrypt.compare(password, user.password); - if (!user || !isPasswordValid) { - throw ApiError.badRequest('Email or password is incorrect'); + if (!isPasswordValid) { + return next(ApiError.badRequest('Email or password is incorrect')); + } + + if (user.activationToken) { + return next(ApiError.badRequest('Please activate your email first')); } await generateTokens(res, user); @@ -98,13 +115,13 @@ const login = async (req, res) => { res.redirect(process.env.CLIENT_HOST + '/profile'); }; -const logout = async (req, res) => { +const logout = async (req, res, next) => { const { refreshToken } = req.cookies; const userData = jwtService.verifyRefresh(refreshToken); if (!userData || !refreshToken) { - throw ApiError.unAuthorized(); + return next(ApiError.unAuthorized()); } await tokenService.remove(userData.id); @@ -112,14 +129,14 @@ const logout = async (req, res) => { res.redirect(process.env.CLIENT_HOST + '/login'); }; -const refresh = async (req, res) => { +const refresh = async (req, res, next) => { const { refreshToken } = req.cookies; const userData = jwtService.verifyRefresh(refreshToken); const token = await tokenService.getByToken(refreshToken); if (!userData || !token) { - throw ApiError.unAuthorized(); + return next(ApiError.unAuthorized()); } const user = await userService.findByEmail(userData.email); @@ -146,13 +163,17 @@ export const generateTokens = async (res, user) => { }); }; -const resetPassword = async (req, res) => { +const resetPassword = async (req, res, next) => { const { email } = req.body; const user = await userService.findByEmail(email); + const erorrs = { + email: 'Email is required', + }; + if (!user) { - throw ApiError.notFound(); + return next(ApiError.notFound(erorrs)); } const confirmToken = uuidv4(); @@ -168,25 +189,25 @@ const resetPassword = async (req, res) => { const confirmResetPassword = async (req, res, next) => { const { confirmToken } = req.params; - const { newPassword1, newPassword2 } = req.body; + const { newPassword, confirmedPassword } = req.body; const user = await User.findOne({ where: { confirmToken } }); if (!user) { - throw ApiError.notFound(); + return next(ApiError.notFound({ message: 'User not found' })); } if (user.confirmTokenExpiry < new Date()) { return next(ApiError.badRequest({ message: 'Token expired' })); } - if (newPassword1.trim() !== newPassword2.trim()) { + if (newPassword.trim() !== confirmedPassword.trim()) { return next( ApiError.badRequest({ newPassword: 'New passwords do not match' }), ); } - const isSamePassword = await bcrypt.compare(newPassword1, user.password); + const isSamePassword = await bcrypt.compare(confirmedPassword, user.password); if (isSamePassword) { return next( @@ -196,7 +217,7 @@ const confirmResetPassword = async (req, res, next) => { ); } - user.password = await bcrypt.hash(newPassword1, 10); + user.password = await bcrypt.hash(newPassword, 10); user.confirmToken = null; user.confirmTokenExpiry = null; await user.save(); diff --git a/src/controllers/profile.controller.js b/src/controllers/profile.controller.js index bd690dd7..775818c5 100644 --- a/src/controllers/profile.controller.js +++ b/src/controllers/profile.controller.js @@ -26,32 +26,30 @@ const changeName = async (req, res) => { }; const changePassword = async (req, res, next) => { - const { oldPassword, newPassword1, newPassword2 } = req.body; + const { oldPassword, newPassword, confirmedPassword } = req.body; const user = await userService.findByEmail(req.user.email); const isPasswordValid = await bcrypt.compare(oldPassword, user.password); + if (!oldPassword || !newPassword || !confirmedPassword) { + return next(ApiError.badRequest('All password fields are required')); + } + if (!isPasswordValid) { - return next( - ApiError.badRequest({ oldPassword: 'Old password is not correct' }), - ); + return next(ApiError.badRequest('Old password is not correct')); } - if (newPassword1.trim() !== newPassword2.trim()) { - return next( - ApiError.badRequest({ newPassword: 'New passwords do not match' }), - ); + if (newPassword !== confirmedPassword) { + return next(ApiError.badRequest('New passwords do not match')); } - if (newPassword1.trim() === oldPassword.trim()) { + if (newPassword === oldPassword) { return next( - ApiError.badRequest({ - newPassword: 'New password must be different from old password', - }), + ApiError.badRequest('New password must be different from old password'), ); } - const password = await bcrypt.hash(newPassword1, 10); + const password = await bcrypt.hash(newPassword, 10); user.password = password; await user.save(); @@ -62,6 +60,22 @@ const changePassword = async (req, res, next) => { const changeEmail = async (req, res, next) => { const { password, newEmail } = req.body; + if (!password || !newEmail) { + return next(ApiError.badRequest('Password and new email are required')); + } + + if (newEmail === req.user.email) { + return next( + ApiError.badRequest('New email must be different from the current one'), + ); + } + + const existingUser = await userService.findByEmail(newEmail); + + if (existingUser) { + return next(ApiError.badRequest('This email is already in use')); + } + const user = await userService.findByEmail(req.user.email); const isPasswordValid = await bcrypt.compare(password, user.password); @@ -72,9 +86,10 @@ const changeEmail = async (req, res, next) => { } const confirmToken = uuidv4(); + const TTL = new Date(Date.now() + 1 * 60 * 60 * 1000); user.confirmToken = confirmToken; - user.confirmTokenExpiry = new Date(Date.now() + 1 * 60 * 60 * 1000); + user.confirmTokenExpiry = TTL; user.pendingEmail = newEmail; await user.save(); @@ -89,7 +104,9 @@ const confirmEmail = async (req, res, next) => { const user = await User.findOne({ where: { confirmToken } }); if (!user) { - throw ApiError.notFound(); + throw ApiError.notFound({ + message: 'Invalid or non-existent confirmation token', + }); } if (user.confirmTokenExpiry < new Date()) { diff --git a/src/index.js b/src/index.js index bfcd3233..dbd2f28a 100644 --- a/src/index.js +++ b/src/index.js @@ -29,7 +29,7 @@ app.use('/api', authRouter); app.use('/api/profile', profileRouter); app.use((req, res, next) => { - next(ApiError.notFound()); + return next(ApiError.notFound()); }); app.use(errorMiddleware); diff --git a/src/middlewares/errorMiddleware.js b/src/middlewares/errorMiddleware.js index bc47c9da..205ebf04 100644 --- a/src/middlewares/errorMiddleware.js +++ b/src/middlewares/errorMiddleware.js @@ -9,5 +9,5 @@ export const errorMiddleware = (error, req, res, next) => { } res.statusCode = 500; - res.send({ message: 'Server error' }); + res.send({ message: error }); }; diff --git a/src/routes/auth.route.js b/src/routes/auth.route.js index 7f64f5b4..4b8d8410 100644 --- a/src/routes/auth.route.js +++ b/src/routes/auth.route.js @@ -16,6 +16,7 @@ authRouter.post('/login', catchError(authController.login)); authRouter.get('/refresh', catchError(authController.refresh)); authRouter.post('/logout', authMiddleware, catchError(authController.logout)); authRouter.post('/reset-password', catchError(authController.resetPassword)); + authRouter.post( '/reset-password/:confirmToken', catchError(authController.confirmResetPassword), diff --git a/src/routes/oauth.route.js b/src/routes/oauth.route.js index d4fd381a..b3bf1271 100644 --- a/src/routes/oauth.route.js +++ b/src/routes/oauth.route.js @@ -1,12 +1,13 @@ import express from 'express'; import passport from '../utils/passport.js'; -import { generateTokens } from '../controllers/auth.controller.js'; import { tokenService } from '../services/token.service.js'; import { userService } from '../services/user.service.js'; import { jwtService } from '../services/jwt.service.js'; export const oauthRouter = express.Router(); +const TTL = 30 * 24 * 60 * 60 * 1000; + oauthRouter.get( '/auth/google', passport.authenticate('google', { @@ -29,7 +30,7 @@ oauthRouter.get( await tokenService.save(normalizedUser.id, refreshToken); res.cookie('refreshToken', refreshToken, { - maxAge: 30 * 24 * 60 * 60 * 1000, + maxAge: TTL, httpOnly: true, }); @@ -44,13 +45,24 @@ oauthRouter.get( passport.authenticate('github', { scope: ['user:email'], session: false }), ); -oauthRouter.get('/auth/github/callback', - passport.authenticate('github', { session: false, failureRedirect: `${process.env.CLIENT_HOST}/login` }), +oauthRouter.get( + '/auth/github/callback', + passport.authenticate('github', { + session: false, + failureRedirect: `${process.env.CLIENT_HOST}/login`, + }), async (req, res) => { const normalizedUser = userService.normalize(req.user); const accessToken = jwtService.sign(normalizedUser); const refreshToken = jwtService.signRefresh(normalizedUser); + const baseURL = new URL(`${process.env.CLIENT_HOST}/oauth`); + + baseURL.searchParams.set('accessToken', accessToken); + baseURL.searchParams.set('user', JSON.stringify(normalizedUser)); + + const accessTokenURL = baseURL.toString(); + await tokenService.save(normalizedUser.id, refreshToken); res.cookie('refreshToken', refreshToken, { @@ -58,6 +70,6 @@ oauthRouter.get('/auth/github/callback', httpOnly: true, }); - res.redirect(`${process.env.CLIENT_HOST}/oauth?accessToken=${accessToken}&user=${encodeURIComponent(JSON.stringify(normalizedUser))}`); - } + res.redirect(accessTokenURL); + }, ); diff --git a/src/routes/profile.route.js b/src/routes/profile.route.js index 0e8ac72c..88014a13 100644 --- a/src/routes/profile.route.js +++ b/src/routes/profile.route.js @@ -12,11 +12,13 @@ profileRouter.post( authMiddleware, catchError(profileController.changeName), ); + profileRouter.post( '/change-email', authMiddleware, catchError(profileController.changeEmail), ); + profileRouter.get( '/change-email/:confirmToken', authMiddleware, diff --git a/src/services/user.service.js b/src/services/user.service.js index 1f23600f..30a8071b 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -12,7 +12,9 @@ function profileNormalize({ id, name, email }) { } async function findByEmail(email) { - return await User.findOne({ where: { email } }); + const user = await User.findOne({ where: { email } }); + + return user; } async function register(name, email, password) { From bba213cb86a9a84f8dbb12578175fad8638cd295 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Thu, 18 Jun 2026 16:08:33 +0300 Subject: [PATCH 19/19] added error handling --- client/src/api/auth.js | 6 +++--- client/src/api/index.js | 5 +++++ client/src/api/profile.js | 6 +++--- client/src/main.js | 1 + client/src/pages/ConfirmResetPasswordPage.vue | 12 +++++------ client/src/pages/LoginPage.vue | 20 +++++++++---------- client/src/pages/ProfilePage.vue | 19 ++++++++---------- client/src/pages/ResetPasswordPage.vue | 8 +++++--- client/src/router/index.js | 18 ++++++++++++++--- client/src/stores/auth.js | 16 +++++++++------ 10 files changed, 66 insertions(+), 45 deletions(-) diff --git a/client/src/api/auth.js b/client/src/api/auth.js index d8aab162..2a2a2779 100644 --- a/client/src/api/auth.js +++ b/client/src/api/auth.js @@ -12,9 +12,9 @@ export const authApi = { resetPassword: (email) => api.post('/api/reset-password', { email }), - confirmResetPassword: (confirmToken, newPassword1, newPassword2) => + confirmResetPassword: (confirmToken, newPassword, confirmedPassword) => api.post(`/api/reset-password/${confirmToken}`, { - newPassword1, - newPassword2, + newPassword, + confirmedPassword, }), }; diff --git a/client/src/api/index.js b/client/src/api/index.js index 7b3b2903..c27f4ca7 100644 --- a/client/src/api/index.js +++ b/client/src/api/index.js @@ -7,9 +7,11 @@ const api = axios.create({ api.interceptors.request.use((config) => { const auth = useAuthStore(); + if (auth.accessToken) { config.headers.Authorization = `Bearer ${auth.accessToken}`; } + return config; }); @@ -25,14 +27,17 @@ api.interceptors.response.use( !original.url.includes('/refresh') ) { original._retry = true; + try { await auth.refresh(); original.headers.Authorization = `Bearer ${auth.accessToken}`; + return api(original); } catch { auth.logout(); } } + return Promise.reject(error); }, ); diff --git a/client/src/api/profile.js b/client/src/api/profile.js index e57ca151..8e49312f 100644 --- a/client/src/api/profile.js +++ b/client/src/api/profile.js @@ -5,11 +5,11 @@ export const profileApi = { changeName: (name) => api.post('/api/profile/change-name', { name }), - changePassword: (oldPassword, newPassword1, newPassword2) => + changePassword: (oldPassword, newPassword, confirmedPassword) => api.post('/api/profile/change-password', { oldPassword, - newPassword1, - newPassword2, + newPassword, + confirmedPassword, }), changeEmail: (password, newEmail) => diff --git a/client/src/main.js b/client/src/main.js index f5e32ec4..76420cc3 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -4,6 +4,7 @@ import App from './App.vue'; import router from './router/index.js'; const app = createApp(App); + app.use(createPinia()); app.use(router); app.mount('#app'); diff --git a/client/src/pages/ConfirmResetPasswordPage.vue b/client/src/pages/ConfirmResetPasswordPage.vue index e210cf5b..b157499b 100644 --- a/client/src/pages/ConfirmResetPasswordPage.vue +++ b/client/src/pages/ConfirmResetPasswordPage.vue @@ -4,8 +4,8 @@ import { useRoute } from 'vue-router'; import { authApi } from '../api/auth.js'; const route = useRoute(); -const newPassword1 = ref(''); -const newPassword2 = ref(''); +const newPassword = ref(''); +const confirmedPassword = ref(''); const loading = ref(false); const success = ref(false); const errors = ref(null); @@ -16,8 +16,8 @@ const submit = async () => { try { await authApi.confirmResetPassword( route.params.confirmToken, - newPassword1.value, - newPassword2.value, + newPassword.value, + confirmedPassword.value, ); success.value = true; } catch (err) { @@ -49,7 +49,7 @@ const submit = async () => {
{
{

Welcome back

Sign in to your account

-
- {{ auth.error.general }} -
-
-

- {{ auth.error.email }} +

+ {{ auth.error.errors.email }}

@@ -45,7 +41,7 @@ const submit = async () => { v-model="password" :type="showPassword ? 'text' : 'password'" placeholder="••••••••" - :class="{ error: auth.error?.password }" + :class="{ error: auth.error?.errors?.password }" @keyup.enter="submit" />
-

- {{ auth.error.password }} +

+ {{ auth.error.errors.password }}

+
+ {{ auth.error.message }} +
+ diff --git a/client/src/pages/ProfilePage.vue b/client/src/pages/ProfilePage.vue index 40dab7d6..a1ffa2d9 100644 --- a/client/src/pages/ProfilePage.vue +++ b/client/src/pages/ProfilePage.vue @@ -61,8 +61,8 @@ const submitEmail = async () => { }; const oldPassword = ref(''); -const newPassword1 = ref(''); -const newPassword2 = ref(''); +const newPassword = ref(''); +const confirmedPassword = ref(''); const passLoading = ref(false); const passSuccess = ref(false); const passErrors = ref(null); @@ -74,13 +74,13 @@ const submitPassword = async () => { try { await profileApi.changePassword( oldPassword.value, - newPassword1.value, - newPassword2.value, + newPassword.value, + confirmedPassword.value, ); passSuccess.value = true; oldPassword.value = ''; - newPassword1.value = ''; - newPassword2.value = ''; + newPassword.value = ''; + confirmedPassword.value = ''; } catch (err) { passErrors.value = err.response?.data?.errors || { general: err.response?.data?.message || 'Failed to change password', @@ -203,7 +203,6 @@ onMounted(async () => { v-model="oldPassword" type="password" placeholder="••••••••" - :class="{ error: passErrors?.oldPassword }" />

{{ passErrors.oldPassword }} @@ -212,22 +211,20 @@ onMounted(async () => {

- {{ passErrors.newPassword }}