diff --git a/.env b/.env new file mode 100644 index 00000000..99fe8083 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +PORT=5700, +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=123 +POSTGRES_DB=postgres +JWT_SECRET_KEY=itlv&80f8 + diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000..4d21aacc --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,14 @@ +module.exports = { + extends: '@mate-academy/eslint-config', + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + }, + env: { + jest: true, + }, + rules: { + 'no-proto': 0, + }, + plugins: ['jest', '@typescript-eslint'], +}; diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index f44c7a1d..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - extends: '@mate-academy/eslint-config', - env: { - jest: true - }, - rules: { - 'no-proto': 0 - }, - plugins: ['jest'] -}; diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 00000000..bb13dfc4 --- /dev/null +++ b/.github/workflows/test.yml-template @@ -0,0 +1,23 @@ +name: Test + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore index ed48a299..bd6a178a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,3 @@ node_modules # MacOS .DS_Store - -# env files -*.env -.env* diff --git a/package-lock.json b/package-lock.json index 288d83dd..f9f767d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,31 @@ "version": "1.0.0", "hasInstallScript": true, "license": "GPL-3.0", + "dependencies": { + "@types/bcrypt": "^6.0.0", + "@types/jsonwebtoken": "^9.0.10", + "bcrypt": "^6.0.0", + "dotenv": "^17.2.3", + "jsonwebtoken": "^9.0.3", + "nodemailer": "^8.0.0", + "pg": "^8.17.2", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.7", + "zod": "^4.3.6" + }, "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", + "@types/nodemailer": "^7.0.9", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", "jest": "^29.7.0", - "prettier": "^3.3.2" + "prettier": "^3.3.2", + "tsx": "^4.19.0", + "typescript": "^5.5.0" } }, "node_modules/@ampproject/remapping": { @@ -59,6 +76,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -535,6 +553,448 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -563,10 +1023,11 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -1467,10 +1928,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.6.tgz", - "integrity": "sha512-b4om/whj4G9emyi84ORE3FRZzCRwRIesr8tJHXa8EvJdOaAPDpzcJ8A0sFfMsWH9NUOVmOwkBtOXDu5eZZ00Ig==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", @@ -1487,6 +1949,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", @@ -1536,7 +2008,6 @@ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", "dev": true, - "peer": true, "engines": { "node": ">= 18" } @@ -1565,7 +2036,6 @@ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", "dev": true, - "peer": true, "dependencies": { "@octokit/types": "^13.0.0", "universal-user-agent": "^7.0.2" @@ -1579,7 +2049,6 @@ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", "dev": true, - "peer": true, "dependencies": { "@octokit/request": "^9.0.0", "@octokit/types": "^13.0.0", @@ -1593,8 +2062,7 @@ "version": "22.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@octokit/plugin-paginate-rest": { "version": "2.21.3", @@ -1656,7 +2124,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", "dev": true, - "peer": true, "dependencies": { "@octokit/endpoint": "^10.0.0", "@octokit/request-error": "^6.0.1", @@ -1672,7 +2139,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", "dev": true, - "peer": true, "dependencies": { "@octokit/types": "^13.0.0" }, @@ -1860,7 +2326,6 @@ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", "dev": true, - "peer": true, "dependencies": { "@octokit/openapi-types": "^22.2.0" } @@ -1968,6 +2433,24 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "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,21 +2500,52 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "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" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/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", @@ -2047,14 +2561,80 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", - "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/visitor-keys": "7.17.0" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2064,11 +2644,40 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", - "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || >=20.0.0" }, @@ -2078,13 +2687,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", - "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/visitor-keys": "7.17.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2106,10 +2716,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2119,6 +2730,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2130,10 +2742,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2142,15 +2755,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", - "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.17.0", - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/typescript-estree": "7.17.0" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2164,12 +2778,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", - "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2185,6 +2800,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -2203,6 +2819,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2331,6 +2948,7 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2620,12 +3238,25 @@ "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", "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -2668,6 +3299,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -2690,6 +3322,12 @@ "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", @@ -3038,12 +3676,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" @@ -3155,6 +3793,7 @@ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -3174,13 +3813,31 @@ "node": ">=6.0.0" } }, - "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, - "engines": { - "node": ">=10" + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, + "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/electron-to-chromium": { @@ -3358,6 +4015,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -3381,6 +4080,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3436,6 +4136,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3526,6 +4227,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -3603,6 +4305,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, + "peer": true, "dependencies": { "eslint-plugin-es": "^3.0.0", "eslint-utils": "^2.0.0", @@ -3653,6 +4356,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz", "integrity": "sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==", "dev": true, + "peer": true, "engines": { "node": ">=6" } @@ -3676,6 +4380,7 @@ "url": "https://feross.org/support" } ], + "peer": true, "peerDependencies": { "eslint": ">=5.0.0" } @@ -4035,16 +4740,17 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -4055,6 +4761,7 @@ "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" }, @@ -4319,6 +5026,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4382,6 +5102,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -4570,6 +5291,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", @@ -5044,6 +5774,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -6673,12 +7404,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.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "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 +7526,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "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", @@ -6820,15 +7654,17 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -6867,11 +7703,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", @@ -6916,6 +7773,15 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "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 +7802,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 +7825,15 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, + "node_modules/nodemailer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-xvVJf/f0bzmNpnRIbhCp/IKxaHgJ6QynvUbLXzzMRPG3LDQr5oXkYuw4uDFyFYs8cge8agwwrJAXZsd4hhMquw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7258,10 +8144,113 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/pg": { + "version": "8.17.2", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", + "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.10.1", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz", + "integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==", + "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.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "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 +8351,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", @@ -7376,6 +8404,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7584,6 +8613,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -7593,6 +8632,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", @@ -7660,6 +8705,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", @@ -7686,6 +8751,89 @@ "semver": "bin/semver.js" } }, + "node_modules/sequelize": { + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "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.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -7855,6 +9003,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", @@ -8096,6 +9253,12 @@ "node": ">=8.0" } }, + "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/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -8162,6 +9325,26 @@ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8297,18 +9480,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "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", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", - "dev": true, - "peer": true + "dev": true }, "node_modules/universalify": { "version": "2.0.1", @@ -8358,6 +9545,15 @@ "punycode": "^2.1.0" } }, + "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==", + "license": "MIT", + "bin": { + "uuid": "dist/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 +9568,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -8580,6 +9785,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", @@ -8658,6 +9872,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", @@ -8711,6 +9934,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 5e195a15..a53ac54f 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "node_auth-app", "version": "1.0.0", + "type": "module", "description": "Auth app", - "main": "src/index.js", + "main": "src/index.ts", "scripts": { "init": "mate-scripts init", - "start": "node src/index.js", + "start": "tsx src/index.ts", "lint": "npm run format && mate-scripts lint", "format": "prettier --ignore-path .prettierignore --write './src/**/*.{js,ts}'", "test:only": "mate-scripts test", @@ -17,14 +18,31 @@ "license": "GPL-3.0", "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", + "@types/nodemailer": "^7.0.9", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", "jest": "^29.7.0", - "prettier": "^3.3.2" + "prettier": "^3.3.2", + "tsx": "^4.19.0", + "typescript": "^5.5.0" }, "mateAcademy": { "projectType": "javascript" + }, + "dependencies": { + "@types/bcrypt": "^6.0.0", + "@types/jsonwebtoken": "^9.0.10", + "bcrypt": "^6.0.0", + "dotenv": "^17.2.3", + "jsonwebtoken": "^9.0.3", + "nodemailer": "^8.0.0", + "pg": "^8.17.2", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.7", + "zod": "^4.3.6" } } diff --git a/src/controllers/acc.ctr.ts b/src/controllers/acc.ctr.ts new file mode 100644 index 00000000..38685fea --- /dev/null +++ b/src/controllers/acc.ctr.ts @@ -0,0 +1,101 @@ +import dto from '../dto/index.ts'; +import { RequestError } from '../errors/index.ts'; +import { mailTemplate } from '../services/email/email.const.ts'; +import srv from '../services/index.ts'; +import { httpStatus, TKN, sch } from '../static/index.ts'; +import type { Ctx, DTOUser } from '../static/types/index.ts'; +import { hashPwd, setTkn, verifyPwd } from './helpers/helpers.ts'; + +async function getAccData(ctx: Ctx) { + const { res, usr } = ctx; + + const dbres = await srv.usr.gbId((usr as DTOUser).id); + + res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(dto.usr(dbres))); +} + +async function deleteAcc(ctx: Ctx) { + const { res, usr } = ctx; + + await srv.usr.dlt((usr as DTOUser).id); + + res.setHeader('Set-Cookie', [ + setTkn(TKN.ACC, '', '0'), + setTkn(TKN.RFR, '', '0'), + ]); + + res.statusCode = httpStatus.nc; + res.end(); +} + +async function patchAcc(ctx: Ctx) { + const { res, body, usr } = ctx; + const { password, email, name } = body; + + const user = await srv.usr.gbId((usr as DTOUser).id); + + const isValid = await verifyPwd(user.password, password); + + if (!isValid) { + throw new RequestError('Invalid credentials', httpStatus.ua); + } + + const payload: { name?: string; email?: string } = {}; + + if (name) { + payload.name = name; + } + + if (email) { + payload.email = email; + } + + const newUsr = await srv.usr.ptch(user.id, payload); + + if (email && email !== user.email) { + await srv.eml.sdTM(user.email, email, mailTemplate.emlChg); + } + + res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(dto.usr(newUsr))); +} + +async function changePass(ctx: Ctx) { + const { res, body, usr } = ctx; + const { oldPwd, newPwd } = body; + + const user = await srv.usr.gbId((usr as DTOUser).id); + + const isValid = await verifyPwd(user.password, oldPwd); + + if (!isValid) { + throw new RequestError('Invalid credentials', httpStatus.ua); + } + + const hashedPwd = await hashPwd(newPwd); + + await srv.usr.ptch(user.id, { password: hashedPwd }); + + res.setHeader('Set-Cookie', [ + setTkn(TKN.ACC, '', '0'), + setTkn(TKN.RFR, '', '0'), + ]); + + res.setHeader('Content-Type', 'application/json'); + + res.statusCode = httpStatus.ok; + + res.end(JSON.stringify({ message: 'Password changed successfully' })); +} + +const acc = { + get: getAccData, + del: deleteAcc, + patch: patchAcc, + pwd: changePass, +}; + +export default acc; diff --git a/src/controllers/auth.ctr.ts b/src/controllers/auth.ctr.ts new file mode 100644 index 00000000..1d4dddea --- /dev/null +++ b/src/controllers/auth.ctr.ts @@ -0,0 +1,100 @@ +import bcrypt from 'bcrypt'; +import { RequestError } from '../errors/index.ts'; +import srv from '../services/index.ts'; +import { httpStatus, TKN, sch } from '../static/index.ts'; +import utl from '../utils/index.ts'; +import type { Ctx } from '../static/types/index.ts'; +import { ckNms, handleTokens, setTkn } from './helpers/helpers.ts'; + +async function manual(ctx: Ctx): Promise { + const { res, body } = ctx; + const { email, password } = body; + // check usr activation + const user = await srv.usr.gbEm(email); + + if (!user.activated) { + throw new RequestError( + 'Please activate your account before logging in', + httpStatus.ua, + ); + } + + // compare pw + + const isValid = await bcrypt.compare(password, user.password); + + if (!isValid) { + throw new RequestError('Invalid credentials', httpStatus.ua); + } + + // create tkns + + const { id, name } = user; + const payload = { id, name, email }; + + await handleTokens(res, payload); + + res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ message: 'Authorized', user: { id, name, email } })); +} + +async function refresh(ctx: Ctx) { + const { req, res } = ctx; + const cookies = utl.prsCks(req); + const token = cookies[ckNms[TKN.RFR]]; + + if (!token) { + throw new RequestError('No token provided', httpStatus.ua); + } + + const { type, ...pl } = utl.jwt.ver(token); + + if (type !== TKN.RFR) { + throw new RequestError('Invalid token type', httpStatus.ua); + } + + const dbTok = await srv.tkn.gBTkn(token); + + // delete token + await srv.tkn.dlt(dbTok.id); + + await handleTokens(res, pl); + + // end res + res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ message: 'Authorized', user: { ...pl } })); +} + +async function logout(ctx: Ctx) { + const { req, res } = ctx; + const cookies = utl.prsCks(req); + + const refToken = cookies[ckNms[TKN.RFR]]; + + if (refToken) { + try { + const dbTok = await srv.tkn.gBTkn(refToken); + + await srv.tkn.dlt(dbTok.id); + } catch {} + } + + res.setHeader('Set-Cookie', [ + setTkn(TKN.ACC, '', '0'), + setTkn(TKN.RFR, '', '0'), + ]); + + res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ message: 'Logged out' })); +} + +const auth = { + man: manual, + rfr: refresh, + lgt: logout, +}; + +export default auth; diff --git a/src/controllers/helpers/helpers.ts b/src/controllers/helpers/helpers.ts new file mode 100644 index 00000000..e51244bc --- /dev/null +++ b/src/controllers/helpers/helpers.ts @@ -0,0 +1,45 @@ +import http from 'http'; +import bcrypt from 'bcrypt'; +import srv from '../../services/index.ts'; +import { TKN, TOKEN_EXPIRY } from '../../static/index.ts'; +import type { DTOUser } from '../../static/types/index.ts'; +import utl from '../../utils/index.ts'; + +const SALT_ROUNDS = 10; + +const ckNms = { + [TKN.ACC]: 'access_token', + [TKN.RFR]: 'refresh_token', +}; + +const setTkn = (t: typeof TKN.ACC | typeof TKN.RFR, s: string, age: string) => + `${ckNms[t]}=${s}; HttpOnly; Path=/; Max-Age=${age}; SameSite=Strict`; + +const verifyPwd = async (pwdhash: string, pwd: string): Promise => { + return bcrypt.compare(pwdhash, pwd); +}; + +const hashPwd = async (pwd: string) => { + return bcrypt.hash(pwd, SALT_ROUNDS); +}; + +async function handleTokens(res: http.ServerResponse, pl: DTOUser) { + const accTkn = utl.jwt.create[TKN.ACC](pl); + const refTkn = utl.jwt.create[TKN.RFR](pl); + + const createPayload = { + userId: pl.id, + token: refTkn.token, + type: TKN.RFR, + expiresAt: refTkn.expiresAt, + }; + + await srv.tkn.crt(createPayload); + + res.setHeader('Set-Cookie', [ + setTkn(TKN.ACC, accTkn.token, TOKEN_EXPIRY[TKN.ACC][1]), + setTkn(TKN.RFR, refTkn.token, TOKEN_EXPIRY[TKN.RFR][1]), + ]); +} + +export { ckNms, setTkn, verifyPwd, hashPwd, handleTokens }; diff --git a/src/controllers/idx.ctr.ts b/src/controllers/idx.ctr.ts new file mode 100644 index 00000000..a762148b --- /dev/null +++ b/src/controllers/idx.ctr.ts @@ -0,0 +1,11 @@ +import http from 'http'; + +const index = '

some-html

'; + +const idx = (res: http.ServerResponse) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.end(index); +}; + +export default idx; diff --git a/src/controllers/index.ts b/src/controllers/index.ts new file mode 100644 index 00000000..4ae96542 --- /dev/null +++ b/src/controllers/index.ts @@ -0,0 +1,15 @@ +import idx from './idx.ctr.ts'; +import auth from './auth.ctr.ts'; +import reg from './reg.ctr.ts'; +import acc from './acc.ctr.ts'; +import rst from './rst.ctr.ts'; + +const ctr = { + idx: idx, + auth: auth, + reg: reg, + acc: acc, + res: rst, +}; + +export default ctr; diff --git a/src/controllers/reg.ctr.ts b/src/controllers/reg.ctr.ts new file mode 100644 index 00000000..68aab9b3 --- /dev/null +++ b/src/controllers/reg.ctr.ts @@ -0,0 +1,89 @@ +import dto from '../dto/index.ts'; +import { RequestError } from '../errors/index.ts'; +import { mailTemplate } from '../services/email/email.const.ts'; +import srv from '../services/index.ts'; +import { httpStatus, sch, TKN } from '../static/index.ts'; +import type { Ctx } from '../static/types/index.ts'; +import utl from '../utils/index.ts'; + +import { handleTokens, hashPwd } from './helpers/helpers.ts'; + +async function register(ctx: Ctx): Promise { + const { res, body } = ctx; + const { name, email, password } = body; + + const exists = await srv.usr.exBEm(email); + + if (exists) { + throw new RequestError( + `User with email ${email} already exists`, + httpStatus.br, + ); + } + + const hashedPwd = await hashPwd(password); + + const newUsr = await srv.usr.crt({ + name, + email, + password: hashedPwd, + }); + + const { token, expiresAt } = utl.jwt.sign(dto.usr(newUsr), TKN.ACT); + + const createPL = { + userId: newUsr.id, + token: token, + type: TKN.ACT, + expiresAt: expiresAt, + }; + + const sent = await srv.eml.sdTM(email, token, mailTemplate.act); + + if (sent) { + await srv.tkn.crt(createPL); + } + + res.statusCode = httpStatus.cr; + res.setHeader('Content-Type', 'application/json'); + + res.end( + JSON.stringify({ + message: 'User created. Check your email for activation link.', + user: { id: newUsr.id, name, email }, + }), + ); +} + +async function activate(ctx: Ctx) { + const { res, param } = ctx; + + const token = await srv.tkn.gBTkn(param as string); + + if (token.type !== TKN.ACT) { + throw new RequestError('Invalid token type', httpStatus.br); + } + + if (new Date(token.expiresAt) < new Date()) { + throw new RequestError('Token expired', httpStatus.br); + } + + const user = await srv.usr.ptch(token.userId, { activated: true }); + + await srv.tkn.dlt(token.id); + + const payload = dto.usr(user); + + await handleTokens(res, payload); + + res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ message: 'Activated', user: payload })); +} + +const reg = { + reg: register, + act: activate, +}; + +export default reg; diff --git a/src/controllers/rst.ctr.ts b/src/controllers/rst.ctr.ts new file mode 100644 index 00000000..b7d48787 --- /dev/null +++ b/src/controllers/rst.ctr.ts @@ -0,0 +1,89 @@ +import dto from '../dto/index.ts'; +import { RequestError } from '../errors/index.ts'; +import { mailTemplate } from '../services/email/email.const.ts'; +import srv from '../services/index.ts'; +import { httpStatus, TKN, sch } from '../static/index.ts'; +import { type Ctx } from '../static/types/index.ts'; +import utl from '../utils/index.ts'; +import { hashPwd } from './helpers/helpers.ts'; + +async function requestReset(ctx: Ctx) { + const { res, body } = ctx; + const { email } = body; + + res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); + + res.end( + JSON.stringify({ + message: 'Check your inbox for password reset link.', + }), + ); + + try { + const user = await srv.usr.gbEm(email); + + await srv.tkn.dltBUID(user.id, TKN.PWR); + + const { token, expiresAt } = utl.jwt.sign(dto.usr(user), TKN.PWR); + + const createPl = { + userId: user.id, + token: token, + type: TKN.PWR, + expiresAt: expiresAt, + }; + + const mail = await srv.eml.sdTM(user.email, token, mailTemplate.res); + + if (mail) { + await srv.tkn.crt(createPl); + } + } catch (e) { + if (e instanceof RequestError && e.statusCode === httpStatus.nf) { + // eslint-disable-next-line no-console + console.warn('UID was not found'); + } else { + throw e; + } + } +} + +async function processReset(ctx: Ctx) { + const { res, body } = ctx; + const { token, newPwd } = body; + + const { type, ...payload } = utl.jwt.ver(token); + + if (type !== TKN.PWR) { + throw new RequestError('Invalid token type', httpStatus.br); + } + + const dbToken = await srv.tkn.gBTkn(token); + + if (dbToken.userId !== payload.id) { + throw new RequestError('Token mismatch', httpStatus.br); + } + + const hashedPwd = await hashPwd(newPwd); + + await srv.usr.ptch(payload.id, { password: hashedPwd }); + + await srv.tkn.dlt(dbToken.id); + + res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); + + res.end( + JSON.stringify({ + message: 'Password reset successfully. You can now login.', + }), + ); +} + +const rst = { + req: requestReset, + prc: processReset, +}; + +export default rst; diff --git a/src/createServer.ts b/src/createServer.ts new file mode 100644 index 00000000..b89df1b3 --- /dev/null +++ b/src/createServer.ts @@ -0,0 +1,54 @@ +import http from 'http'; +import { dbSetup } from './db/index.ts'; +import val from './validation/index.ts'; +import mw from './middleware/index.ts'; +import grc from './router/index.ts'; +import type { Ctx } from './static/types/index.ts'; +import utl from './utils/index.ts'; + +export async function createServer() { + await dbSetup(); + + return http.createServer(async (req, res) => { + utl.setCors(res); + + // Handle preflight OPTIONS + if (req.method === 'OPTIONS') { + res.statusCode = 204; + res.end(); + + return; + } + + try { + // validate request + const { endpoint, method, param } = val.req(req); + + // get route config from router + const { auth, schema, ctr } = grc(endpoint, method); + let usr = null; + + // check auth token if router auth === true + if (auth) { + usr = mw.tokenAuth(req); + } + + // validating body by comparing to schema if router schema !== null + const body = schema ? val.bd(await mw.parseB(req), schema) : null; + + // creating ctx for controller + const ctx: Ctx = { + req, + res, + body, + usr, + param, + }; + + // executing controller fn with ctx payload + await ctr(ctx); + } catch (e) { + mw.error(res, e); + } + }); +} diff --git a/src/db/db.ts b/src/db/db.ts new file mode 100644 index 00000000..57825651 --- /dev/null +++ b/src/db/db.ts @@ -0,0 +1,34 @@ +'use strict'; +import { Sequelize } from 'sequelize'; +import { DBError } from '../errors/index.ts'; + +const { + POSTGRES_HOST, + POSTGRES_PORT, + POSTGRES_USER, + POSTGRES_PASSWORD, + POSTGRES_DB, +} = process.env; + +const client = new Sequelize({ + database: POSTGRES_DB || 'postgres', + username: POSTGRES_USER || 'postgres', + host: POSTGRES_HOST || 'localhost', + dialect: 'postgres', + port: Number(POSTGRES_PORT) || 5432, + password: POSTGRES_PASSWORD || '123', +}); + +const dbSetup = async () => { + try { + const { default: DB } = await import('../model/index.ts'); + + for (const model of Object.values(DB)) { + await model.sync(); + } + } catch (error) { + throw new DBError(`Database setup failed: ${error}`); + } +}; + +export { client, dbSetup }; diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 00000000..b7744eef --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1 @@ +export { client, dbSetup } from './db.ts'; diff --git a/src/dto/dto.ts b/src/dto/dto.ts new file mode 100644 index 00000000..c7120dec --- /dev/null +++ b/src/dto/dto.ts @@ -0,0 +1,9 @@ +import type { DBUser, DTOUser } from '../static/types/user.types.ts'; + +const usrDto = (usr: DBUser): DTOUser => { + const { password, activated, createdAt, updatedAt, ...user } = usr; + + return user; +}; + +export { usrDto }; diff --git a/src/dto/index.ts b/src/dto/index.ts new file mode 100644 index 00000000..8c765f4c --- /dev/null +++ b/src/dto/index.ts @@ -0,0 +1,7 @@ +import { usrDto } from './dto.ts'; + +const dto = { + usr: usrDto, +}; + +export default dto; diff --git a/src/errors/errors.ts b/src/errors/errors.ts new file mode 100644 index 00000000..b52ac3c2 --- /dev/null +++ b/src/errors/errors.ts @@ -0,0 +1,26 @@ +import { httpStatus } from '../static/index.ts'; +import type { HTTPStatus } from '../static/types/index.ts'; + +class RequestError extends Error { + statusCode: HTTPStatus; + + constructor(message: string, statusCode: HTTPStatus = httpStatus.br) { + super(message); + this.statusCode = statusCode; + this.name = this.constructor.name; + } +} + +class DBError extends RequestError { + constructor(message: string) { + super(message, httpStatus.se); + } +} + +class AuthError extends RequestError { + constructor(message: string) { + super(message, httpStatus.na); + } +} + +export { RequestError, DBError, AuthError }; diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 00000000..0392d583 --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1 @@ +export { RequestError, DBError, AuthError } from './errors.ts'; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index ad9a93a7..00000000 --- a/src/index.js +++ /dev/null @@ -1 +0,0 @@ -'use strict'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..9a261c20 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,11 @@ +/* eslint-disable no-console */ +'use strict'; +import { createServer } from './createServer.ts'; + +const PORT = process.env.PORT || 5700; + +const server = await createServer(); + +server.listen(PORT, () => { + console.log(`Server is running on: ${PORT}`); +}); diff --git a/src/middleware/bodyParser.ts b/src/middleware/bodyParser.ts new file mode 100644 index 00000000..c0419583 --- /dev/null +++ b/src/middleware/bodyParser.ts @@ -0,0 +1,66 @@ +import http from 'http'; +import { RequestError } from '../errors/index.ts'; +import { httpStatus } from '../static/index.ts'; + +const maxSizeLimit = 1048576; + +async function parseBody(req: http.IncomingMessage, maxSize = maxSizeLimit) { + return new Promise((resolve, reject) => { + let data = ''; + let size = 0; + let settled = false; + + req.on('error', (err) => { + if (!settled) { + settled = true; + reject(err); + } + }); + + req.on('data', (chunk) => { + if (settled) { + return; + } + + size += chunk.length; + + if (size > maxSize && !settled) { + settled = true; + req.destroy(); + reject(new RequestError('Body max size exceeded', httpStatus.br)); + + return; + } + + data += chunk; + }); + + req.on('end', () => { + if (settled) { + return; + } + + try { + settled = true; + + const res = JSON.parse(data); + + resolve(res); + } catch { + if (!settled) { + settled = true; + reject(new RequestError('Expected JSON', httpStatus.br)); + } + } + }); + + req.on('close', () => { + if (!settled) { + settled = true; + reject(new RequestError('Request cancelled', httpStatus.br)); + } + }); + }); +} + +export { parseBody }; diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts new file mode 100644 index 00000000..04592a61 --- /dev/null +++ b/src/middleware/errorHandler.ts @@ -0,0 +1,19 @@ +import http from 'http'; +import { httpStatus } from '../static/index.ts'; +import { RequestError } from '../errors/index.ts'; + +function errorHandler(res: http.ServerResponse, e: unknown) { + res.setHeader('Content-Type', 'text/plain'); + + if (e instanceof RequestError) { + res.statusCode = e.statusCode; + res.end(e.message); + + return; + } + + res.statusCode = httpStatus.se; + res.end('Unexpected server error'); +} + +export default errorHandler; diff --git a/src/middleware/index.ts b/src/middleware/index.ts new file mode 100644 index 00000000..bfb1649a --- /dev/null +++ b/src/middleware/index.ts @@ -0,0 +1,11 @@ +import { parseBody } from './bodyParser.ts'; +import errorHandler from './errorHandler.ts'; +import tokenAuth from './tokenAuth.ts'; + +const mw = { + parseB: parseBody, + error: errorHandler, + tokenAuth: tokenAuth, +}; + +export default mw; diff --git a/src/middleware/tokenAuth.ts b/src/middleware/tokenAuth.ts new file mode 100644 index 00000000..05141c59 --- /dev/null +++ b/src/middleware/tokenAuth.ts @@ -0,0 +1,24 @@ +import http from 'http'; +import utl from '../utils/index.ts'; +import { ckNms } from '../controllers/helpers/helpers.ts'; +import { httpStatus, TKN } from '../static/index.ts'; +import { RequestError } from '../errors/errors.ts'; + +function tokenAuth(req: http.IncomingMessage) { + const cookies = utl.prsCks(req); + const token = cookies[ckNms[TKN.ACC]]; + + if (!token) { + throw new RequestError('No token provided', httpStatus.ua); + } + + const { type, ...pl } = utl.jwt.ver(token); + + if (type !== TKN.ACC) { + throw new RequestError('Invalid token type', httpStatus.ua); + } + + return pl; +} + +export default tokenAuth; diff --git a/src/model/index.ts b/src/model/index.ts new file mode 100644 index 00000000..810b4540 --- /dev/null +++ b/src/model/index.ts @@ -0,0 +1,10 @@ +import User from './user.model.ts'; +import Token from './token.model.ts'; +import { TNAMES } from '../static/index.ts'; + +const DB = { + [TNAMES.USR]: User, + [TNAMES.TKN]: Token, +}; + +export default DB; diff --git a/src/model/token.model.ts b/src/model/token.model.ts new file mode 100644 index 00000000..3de789cf --- /dev/null +++ b/src/model/token.model.ts @@ -0,0 +1,50 @@ +import { client } from '../db/db.ts'; +import { DataTypes } from 'sequelize'; +import { TNAMES, fnames, TKN } from '../static/index.ts'; + +const nms = fnames[TNAMES.TKN]; + +const Token = client.define( + 'Token', + { + [nms.id]: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + [nms.usr]: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: TNAMES.USR, + key: fnames[TNAMES.USR].id, + }, + onDelete: 'CASCADE', + }, + [nms.token]: { + type: DataTypes.TEXT, + allowNull: false, + unique: true, + }, + [nms.type]: { + type: DataTypes.ENUM(TKN.ACT, TKN.PWR, TKN.RFR), + allowNull: false, + }, + [nms.exp]: { + type: DataTypes.DATE, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + field: 'created_at', + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }, + { + tableName: TNAMES.TKN, + updatedAt: false, + }, +); + +export default Token; diff --git a/src/model/user.model.ts b/src/model/user.model.ts new file mode 100644 index 00000000..1584880d --- /dev/null +++ b/src/model/user.model.ts @@ -0,0 +1,52 @@ +import { client } from '../db/db.ts'; +import { DataTypes } from 'sequelize'; +import { TNAMES, fnames } from '../static/index.ts'; + +const nms = fnames[TNAMES.USR]; + +const User = client.define( + 'User', + { + [nms.id]: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + [nms.name]: { + type: DataTypes.STRING, + allowNull: false, + }, + [nms.email]: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + [nms.pwd]: { + type: DataTypes.STRING, + allowNull: false, + }, + [nms.act]: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + createdAt: { + type: DataTypes.DATE, + field: 'created_at', + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + field: 'updated_at', + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }, + { + tableName: TNAMES.USR, + timestamps: true, + }, +); + +export default User; diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 00000000..0c634083 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,2 @@ +import getRouteConfig from './router.ts'; +export default getRouteConfig; diff --git a/src/router/router.ts b/src/router/router.ts new file mode 100644 index 00000000..4540844b --- /dev/null +++ b/src/router/router.ts @@ -0,0 +1,64 @@ +import ctr from '../controllers/index.ts'; +import { RequestError } from '../errors/index.ts'; +import { ep, httpStatus, mthd, sch } from '../static/index.ts'; +import type { Schema, Ctx, Endpoint, Method } from '../static/types/index.ts'; + +type Entr = { + auth: boolean; + schema: S; + ctr: (args: Ctx) => void | Promise; +}; + +const routeMap = { + [ep.idx]: { + [mthd.get]: { auth: false, schema: null, ctr: ctr.idx }, + }, + [ep.auth]: { + [mthd.post]: { auth: false, schema: sch.auth, ctr: ctr.auth.man }, + }, + [ep.refr]: { + [mthd.post]: { auth: false, schema: null, ctr: ctr.auth.rfr }, + }, + [ep.lgt]: { + [mthd.patch]: { auth: true, schema: null, ctr: ctr.auth.lgt }, + }, + [ep.reg]: { + [mthd.post]: { auth: false, schema: sch.reg, ctr: ctr.reg.reg }, + }, + [ep.act]: { + [mthd.get]: { auth: false, schema: null, ctr: ctr.reg.act }, + }, + [ep.prf]: { + [mthd.get]: { auth: true, schema: null, ctr: ctr.acc.get }, + [mthd.del]: { auth: true, schema: null, ctr: ctr.acc.del }, + [mthd.patch]: { auth: true, schema: sch.upd, ctr: ctr.acc.patch }, + }, + [ep.pwc]: { + [mthd.patch]: { auth: true, schema: sch.pwdUpd, ctr: ctr.acc.pwd }, + }, + [ep.pwrr]: { + [mthd.post]: { auth: false, schema: sch.pwdReq, ctr: ctr.res.req }, + }, + [ep.pwrc]: { + [mthd.post]: { auth: false, schema: sch.pwdRes, ctr: ctr.res.prc }, + }, +} as const; + +const getRouteConfig = ( + e: Endpoint, + m: Method, +): Entr => { + const epRoutes = routeMap[e]; + const conf = epRoutes[m as keyof typeof epRoutes]; + + if (!conf) { + throw new RequestError( + `Method ${m} is not supported for ${e} endpoint`, + httpStatus.na, + ); + } + + return conf; +}; + +export default getRouteConfig; diff --git a/src/services/email/email.const.ts b/src/services/email/email.const.ts new file mode 100644 index 00000000..2a22700e --- /dev/null +++ b/src/services/email/email.const.ts @@ -0,0 +1,52 @@ +import { ep } from '../../static/index.ts'; + +const mailTemplate = { + act: 'activation', + res: 'pwd reset', + emlChg: 'email change', +} as const; + +const activate = (token: string) => { + const link = `${process.env.BASE_URL}${ep.act}?token=${token}`; + + return { + subject: 'Account activation', + html: ` +

Hello!

+

Click the link below to activate your account:

+ ${link} + `, + }; +}; + +const reset = (token: string) => { + const link = `${process.env.BASE_URL}${ep.pwrc}?token=${token}`; + + return { + subject: 'Password reset', + html: ` +

Hello!

+

Click the link below to reset your password:

+ ${link} + `, + }; +}; + +const emailChange = (newEmail: string) => { + return { + subject: 'Email address changed', + html: ` +

Hello!

+

Your email address has been changed to: ${newEmail}

+

If you did not make this change, please contact support immediately.

+ `, + }; +}; + +const getTemplate = { + [mailTemplate.act]: activate, + [mailTemplate.res]: reset, + [mailTemplate.emlChg]: emailChange, +}; + +export { mailTemplate, getTemplate }; diff --git a/src/services/email/email.service.ts b/src/services/email/email.service.ts new file mode 100644 index 00000000..bcfec08d --- /dev/null +++ b/src/services/email/email.service.ts @@ -0,0 +1,41 @@ +import { getTemplate, mailTemplate } from './email.const.ts'; +import transporter from './transporter.ts'; + +async function sendMail( + to: string, + subject: string, + html: string, +): Promise { + await transporter.sendMail({ + from: process.env.SMTP_FROM || 'noreply@example.com', + to: to, + subject: subject, + html: html, + }); +} + +async function sendTemplateMail( + email: string, + token: string, + type: (typeof mailTemplate)[keyof typeof mailTemplate], +): Promise { + const data = getTemplate[type](token); + + try { + await sendMail(email, data.subject, data.html); + + return true; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + + return false; + } +} + +const eml = { + sdM: sendMail, + sdTM: sendTemplateMail, +}; + +export default eml; diff --git a/src/services/email/transporter.ts b/src/services/email/transporter.ts new file mode 100644 index 00000000..47cd8734 --- /dev/null +++ b/src/services/email/transporter.ts @@ -0,0 +1,12 @@ +import nodemailer from 'nodemailer'; + +const transporter = nodemailer.createTransport({ + host: 'smtp.ethereal.email', + port: 587, + auth: { + user: 'palma.spencer@ethereal.email', + pass: 'YhYHdzdxqn1Vcs98GW', + }, +}); + +export default transporter; diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 00000000..7d4c36a8 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,11 @@ +import usr from './user.service.ts'; +import tkn from './token.service.ts'; +import eml from './email/email.service.ts'; + +const srv = { + usr: usr, + tkn: tkn, + eml: eml, +}; + +export default srv; diff --git a/src/services/repo.service.ts b/src/services/repo.service.ts new file mode 100644 index 00000000..47dfcaf2 --- /dev/null +++ b/src/services/repo.service.ts @@ -0,0 +1,86 @@ +import { DBError, RequestError } from '../errors/index.ts'; +import DB from '../model/index.ts'; +import { fnames, httpStatus } from '../static/index.ts'; +import type { Create, DBRes, Tnames, Fnames } from '../static/types/index.ts'; + +function dbHandler( + fn: (...args: TArgs) => Promise, +): (...args: TArgs) => Promise { + return async (...args: TArgs): Promise => { + try { + return await fn(...args); + } catch (e) { + if (e instanceof RequestError) { + throw e; + } + throw new DBError( + `Database operation failed: ${e instanceof Error ? e.message : e}`, + ); + } + }; +} + +const get = async ( + table: T, + key: string, +): Promise => { + const item = await DB[table].findByPk(key); + + if (!item) { + throw new RequestError( + `${table.slice(-1).toUpperCase()} not found: ${key}`, + httpStatus.nf, + ); + } + + return item.toJSON(); +}; + +const del = async (table: Tnames, id: string): Promise => { + const deleted = await DB[table].destroy({ + where: { [fnames[table].id]: id }, + }); + + if (deleted === 0) { + throw new RequestError(`Id not found: ${id}`, httpStatus.nf); + } +}; + +const getByParam = async ( + table: T, + field: Fnames, + query: string | boolean, +): Promise => { + const item = await DB[table].findOne({ where: { [field as string]: query } }); + + if (!item) { + throw new RequestError( + `${table} with ${field}=${query} not found`, + httpStatus.nf, + ); + } + + return item.toJSON(); +}; + +const create = async >( + table: T, + data: Create[T], +): Promise => { + const newObj = await DB[table].create({ + ...data, + }); + + return newObj.toJSON(); +}; + +// aDBH = asyncDBHandler, try/catch cover; + +const base = { + get: dbHandler(get), + del: dbHandler(del), + gBPrm: dbHandler(getByParam), + crt: dbHandler(create), +}; + +export { base, dbHandler }; diff --git a/src/services/token.service.ts b/src/services/token.service.ts new file mode 100644 index 00000000..00975086 --- /dev/null +++ b/src/services/token.service.ts @@ -0,0 +1,21 @@ +import DB from '../model/index.ts'; +import { TNAMES, fnames } from '../static/index.ts'; +import { base } from './repo.service.ts'; +import type { Create, Tokens } from '../static/types/index.ts'; + +const nms = fnames[TNAMES.TKN]; + +const tkn = { + crt: (d: Create[typeof TNAMES.TKN]) => base.crt(TNAMES.TKN, d), + gBTkn: (q: string) => base.gBPrm(TNAMES.TKN, fnames[TNAMES.TKN].token, q), + dlt: (id: string) => base.del(TNAMES.TKN, id), + dltBUID: async (userId: string, type?: Tokens) => { + return DB[TNAMES.TKN].destroy({ + where: type + ? { [nms.usr]: userId, [nms.type]: type } + : { [nms.usr]: userId }, + }); + }, +}; + +export default tkn; diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 00000000..ca1ddf9b --- /dev/null +++ b/src/services/user.service.ts @@ -0,0 +1,35 @@ +import DB from '../model/index.ts'; +import { fnames, httpStatus, TNAMES } from '../static/index.ts'; +import { base, dbHandler } from './repo.service.ts'; +import type { Create, PatchUser } from '../static/types/index.ts'; +import { RequestError } from '../errors/index.ts'; + +async function patchUser(id: string, payload: Partial) { + const user = await base.get(TNAMES.USR, id); + + await DB[TNAMES.USR].update(payload, { where: { id } }); + + return { ...user, ...payload }; +} + +const usr = { + gbId: (id: string) => base.get(TNAMES.USR, id), + gbEm: (e: string) => base.gBPrm(TNAMES.USR, fnames[TNAMES.USR].email, e), + dlt: (id: string) => base.del(TNAMES.USR, id), + crt: (data: Create[typeof TNAMES.USR]) => base.crt(TNAMES.USR, data), + exBEm: async (email: string): Promise => { + try { + await base.gBPrm(TNAMES.USR, fnames[TNAMES.USR].email, email); + + return true; + } catch (e) { + if (e instanceof RequestError && e.statusCode !== httpStatus.se) { + return false; + } + throw e; + } + }, + ptch: dbHandler((id: string, pl: Partial) => patchUser(id, pl)), +}; + +export default usr; diff --git a/src/static/bodySchemas.ts b/src/static/bodySchemas.ts new file mode 100644 index 00000000..9af21a4e --- /dev/null +++ b/src/static/bodySchemas.ts @@ -0,0 +1,87 @@ +import { z } from 'zod'; +import { fnames, TNAMES } from './vocab/dbVocab.ts'; + +const MIN_PWD = 6; +const MIN_STR = 3; + +const authorize = z.object({ + [fnames[TNAMES.USR].email]: z + .string() + .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { + message: 'Invalid email', + }), + [fnames[TNAMES.USR].pwd]: z.string().min(MIN_PWD), +}); + +const registration = z.object({ + [fnames[TNAMES.USR].name]: z.string().min(MIN_STR), + [fnames[TNAMES.USR].email]: z + .string() + .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { + message: 'Invalid email', + }), + [fnames[TNAMES.USR].pwd]: z.string().min(MIN_PWD), +}); + +const profileUpdate = z + .object({ + [fnames[TNAMES.USR].pwd]: z.string().min(MIN_PWD), + [fnames[TNAMES.USR].name]: z.string().min(MIN_STR).optional(), + [fnames[TNAMES.USR].email]: z + .string() + .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { + message: 'Invalid email', + }) + .optional(), + confirmEmail: z.string().optional(), + }) + .refine((data) => data.name || data.email, { + message: 'At least one field (name or email) is required', + }) + .refine((data) => !data.email || data.confirmEmail === data.email, { + message: "Emails don't match", + path: ['confirmEmail'], + }); + +const passwordUpdate = z + .object({ + oldPwd: z.string(), + newPwd: z.string().min(MIN_PWD), + confirmation: z.string(), + }) + .refine((data) => data.confirmation === data.newPwd, { + message: "Passwords don't match", + path: ['confirmation'], + }); + +const requestPwdUpdate = z.object({ + [fnames[TNAMES.USR].email]: z + .string() + .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { + message: 'Invalid email', + }), +}); + +const resolvePwdUpdate = z + .object({ + [fnames[TNAMES.TKN].token]: z.string(), + newPwd: z.string().min(MIN_PWD), + confirmation: z.string(), + }) + .refine((data) => data.confirmation === data.newPwd, { + message: "Passwords don't match", + path: ['confirmation'], + }); + +const sch = { + auth: authorize, + reg: registration, + upd: profileUpdate, + pwdUpd: passwordUpdate, + pwdReq: requestPwdUpdate, + pwdRes: resolvePwdUpdate, +}; + +type Schema = (typeof sch)[keyof typeof sch]; + +export { sch, type Schema }; diff --git a/src/static/endpoints.ts b/src/static/endpoints.ts new file mode 100644 index 00000000..4b432cdb --- /dev/null +++ b/src/static/endpoints.ts @@ -0,0 +1,16 @@ +const ep = { + idx: '/', + auth: '/auth', + refr: '/auth/refresh', + lgt: '/auth/logout', + reg: '/register', + act: '/register/activate', + prf: '/profile', + pwc: '/profile/password', + pwrr: '/password/reset-request', + pwrc: '/password/reset', +} as const; + +type Endpoint = (typeof ep)[keyof typeof ep]; + +export { ep, type Endpoint }; diff --git a/src/static/index.ts b/src/static/index.ts new file mode 100644 index 00000000..d4ed0397 --- /dev/null +++ b/src/static/index.ts @@ -0,0 +1,14 @@ +import { ep } from './endpoints.ts'; +import { TNAMES, fnames, TKN } from './vocab/dbVocab.ts'; +import { mthd, httpStatus } from './vocab/httpVocab.ts'; +import { sch } from './bodySchemas.ts'; +import type { Tokens } from './types/index.ts'; + +const TOKEN_EXPIRY: Record = { + [TKN.ACC]: ['15m', '900'], + [TKN.RFR]: ['7d', '604800'], + [TKN.ACT]: ['24h', '86400'], + [TKN.PWR]: ['1h', '3600'], +}; + +export { TNAMES, TKN, fnames, TOKEN_EXPIRY, mthd, httpStatus, ep, sch }; diff --git a/src/static/types/index.ts b/src/static/types/index.ts new file mode 100644 index 00000000..7a9d634c --- /dev/null +++ b/src/static/types/index.ts @@ -0,0 +1,37 @@ +import http from 'http'; +import { z } from 'zod'; + +import type { DBUser, DTOUser, PatchUser, CreateUser } from './user.types.ts'; +import type { DBToken, CreateTKN } from './token.types.ts'; +import type { Tokens, Tnames, Fnames } from '../vocab/dbVocab.ts'; +import type { Method, HTTPStatus } from '../vocab/httpVocab.ts'; +import type { Schema } from '../bodySchemas.ts'; +import type { Endpoint } from '../endpoints.ts'; +import type { DBRes, Create } from './maps.type.ts'; + +type Ctx = { + req: http.IncomingMessage; + res: http.ServerResponse; + body: S extends z.ZodSchema ? z.infer : null; + usr: DTOUser | null; + param: string | null; +}; + +export type { + DBUser, + DTOUser, + PatchUser, + CreateUser, + DBToken, + CreateTKN, + Ctx, + Tokens, + Tnames, + Method, + HTTPStatus, + Schema, + Endpoint, + DBRes, + Create, + Fnames, +}; diff --git a/src/static/types/maps.type.ts b/src/static/types/maps.type.ts new file mode 100644 index 00000000..9a0ca47d --- /dev/null +++ b/src/static/types/maps.type.ts @@ -0,0 +1,15 @@ +import { TNAMES } from '../vocab/dbVocab.ts'; +import { CreateTKN, DBToken } from './token.types.ts'; +import { CreateUser, DBUser } from './user.types.ts'; + +type DBRes = { + [TNAMES.USR]: DBUser; + [TNAMES.TKN]: DBToken; +}; + +type Create = { + [TNAMES.USR]: CreateUser; + [TNAMES.TKN]: CreateTKN; +}; + +export type { DBRes, Create }; diff --git a/src/static/types/token.types.ts b/src/static/types/token.types.ts new file mode 100644 index 00000000..0af8085e --- /dev/null +++ b/src/static/types/token.types.ts @@ -0,0 +1,18 @@ +import { TKN } from '../index.ts'; +import type { Tokens } from '../vocab/dbVocab.ts'; + +interface DBToken { + id: string; + userId: string; + token: string; + type: Tokens; + expiresAt: Date; + createdAt: Date; +} + +interface CreateTKN extends Pick { + type: Exclude; + expiresAt: string; +} + +export type { DBToken, CreateTKN }; diff --git a/src/static/types/user.types.ts b/src/static/types/user.types.ts new file mode 100644 index 00000000..44f10194 --- /dev/null +++ b/src/static/types/user.types.ts @@ -0,0 +1,17 @@ +interface DBUser { + id: string; + name: string; + email: string; + password: string; + activated: boolean; + createdAt: Date; + updatedAt: Date; +} + +type DTOUser = Pick; + +type PatchUser = Omit; + +type CreateUser = Pick; + +export type { DBUser, DTOUser, PatchUser, CreateUser }; diff --git a/src/static/vocab/dbVocab.ts b/src/static/vocab/dbVocab.ts new file mode 100644 index 00000000..d9b0a04f --- /dev/null +++ b/src/static/vocab/dbVocab.ts @@ -0,0 +1,36 @@ +const TNAMES = { + USR: 'users', + TKN: 'tokens', +} as const; + +type Tnames = (typeof TNAMES)[keyof typeof TNAMES]; + +const fnames = { + [TNAMES.USR]: { + id: 'id', + name: 'name', + email: 'email', + pwd: 'password', + act: 'activated', + }, + [TNAMES.TKN]: { + id: 'id', + usr: 'userId', + token: 'token', + type: 'type', + exp: 'expiresAt', + }, +} as const; + +const TKN = { + ACT: 'activation', + RFR: 'refresh', + PWR: 'password_reset', + ACC: 'access', +} as const; + +type Fnames = (typeof fnames)[T][keyof (typeof fnames)[T]]; +type Tokens = (typeof TKN)[keyof typeof TKN]; + +export { TNAMES, fnames, TKN }; +export type { Tokens, Tnames, Fnames }; diff --git a/src/static/vocab/httpVocab.ts b/src/static/vocab/httpVocab.ts new file mode 100644 index 00000000..b70f17e5 --- /dev/null +++ b/src/static/vocab/httpVocab.ts @@ -0,0 +1,22 @@ +const mthd = { + get: 'GET', + post: 'POST', + del: 'DELETE', + patch: 'PATCH', +} as const; + +const httpStatus = { + ok: 200, + cr: 201, + nc: 204, + br: 400, + ua: 401, + nf: 404, + na: 405, + se: 500, +} as const; + +type Method = (typeof mthd)[keyof typeof mthd]; +type HTTPStatus = (typeof httpStatus)[keyof typeof httpStatus]; + +export { mthd, httpStatus, type Method, type HTTPStatus }; diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts new file mode 100644 index 00000000..2a0b103e --- /dev/null +++ b/src/utils/cookies.ts @@ -0,0 +1,20 @@ +import http from 'http'; + +type Cookies = Record; + +function parseCookies(req: http.IncomingMessage): Cookies { + const cookieHeader = req.headers.cookie || ''; + + return Object.fromEntries( + cookieHeader + .split(';') + .filter(Boolean) + .map((c) => { + const [key, ...val] = c.trim().split('='); + + return [key, val.join('=')]; + }), + ); +} + +export default parseCookies; diff --git a/src/utils/cors.ts b/src/utils/cors.ts new file mode 100644 index 00000000..a491de0a --- /dev/null +++ b/src/utils/cors.ts @@ -0,0 +1,16 @@ +import http from 'http'; + +const CORS_ORIGIN = process.env.CORS_ORIGIN || '*'; + +function setCorsHeaders(res: http.ServerResponse) { + res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN); + + res.setHeader( + 'Access-Control-Allow-Methods', + 'GET, POST, PATCH, DELETE, OPTIONS', + ); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); +} + +export default setCorsHeaders; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..93687e2f --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,11 @@ +import parseCookies from './cookies.ts'; +import jwtAction from './jwt.ts'; +import setCorsHeaders from './cors.ts'; + +const utl = { + jwt: jwtAction, + prsCks: parseCookies, + setCors: setCorsHeaders, +}; + +export default utl; diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts new file mode 100644 index 00000000..4466cd9a --- /dev/null +++ b/src/utils/jwt.ts @@ -0,0 +1,52 @@ +import jwt from 'jsonwebtoken'; +import { RequestError } from '../errors/index.ts'; +import { httpStatus, TKN, TOKEN_EXPIRY } from '../static/index.ts'; +import type { DTOUser, Tokens } from '../static/types/index.ts'; + +const SECRET_KEY = process.env.JWT_SECRET || '7>?~!id(#;fd13/^^$fdkq124<'; + +type Signed = { expiresAt: string; token: string }; + +function signToken(payload: DTOUser, type: Tokens): Signed { + const expiryDuration = TOKEN_EXPIRY[type][0]; + const expirySeconds = Number(TOKEN_EXPIRY[type][1]); + + const token = jwt.sign({ ...payload, type }, SECRET_KEY, { + expiresIn: expiryDuration, + } as jwt.SignOptions); + + const expiresAt = new Date(Date.now() + expirySeconds * 1000).toISOString(); + + return { expiresAt, token }; +} + +function verifyToken(token: string): DTOUser & { type: Tokens } { + try { + const decoded = jwt.verify(token, SECRET_KEY) as DTOUser & { + type: Tokens; + }; + + return decoded; + } catch (error) { + if (error instanceof jwt.TokenExpiredError) { + throw new RequestError('Token has expired', httpStatus.ua); + } + + if (error instanceof jwt.JsonWebTokenError) { + throw new RequestError('Invalid token', httpStatus.br); + } + + throw new RequestError('Token verification failed', httpStatus.br); + } +} + +const jwtAction = { + sign: (pl: DTOUser, tp: Tokens) => signToken(pl, tp), + ver: (tk: string) => verifyToken(tk), + create: { + [TKN.ACC]: (payload: DTOUser): Signed => signToken(payload, TKN.ACC), + [TKN.RFR]: (payload: DTOUser): Signed => signToken(payload, TKN.RFR), + }, +}; + +export default jwtAction; diff --git a/src/validation/index.ts b/src/validation/index.ts new file mode 100644 index 00000000..5b20b449 --- /dev/null +++ b/src/validation/index.ts @@ -0,0 +1,9 @@ +import validateRequest from './validateRequest.ts'; +import validateBody from './validateBody.ts'; + +const val = { + req: validateRequest, + bd: validateBody, +}; + +export default val; diff --git a/src/validation/validateBody.ts b/src/validation/validateBody.ts new file mode 100644 index 00000000..a8a65e4c --- /dev/null +++ b/src/validation/validateBody.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; +import { RequestError } from '../errors/index.ts'; +import { httpStatus } from '../static/index.ts'; + +function validateBody( + body: unknown, + schema: T, +): z.infer { + try { + return schema.parse(body); + } catch (e) { + if (e instanceof z.ZodError) { + throw new RequestError(e.issues[0].message, httpStatus.br); + } + throw e; + } +} + +export default validateBody; diff --git a/src/validation/validateRequest.ts b/src/validation/validateRequest.ts new file mode 100644 index 00000000..77cdf23b --- /dev/null +++ b/src/validation/validateRequest.ts @@ -0,0 +1,53 @@ +import http from 'http'; +import { RequestError } from '../errors/index.ts'; +import { httpStatus, mthd, ep } from '../static/index.ts'; +import type { Endpoint, Method } from '../static/types/index.ts'; + +const REQUIRED_PARAMS: Partial> = { + [ep.act]: 'token', +}; + +const validatePath = (path: string): path is Endpoint => { + return Object.values(ep).some((el) => el === path); +}; + +const validateMethod = (method: string | undefined): method is Method => { + return Object.values(mthd).some((el) => el === method); +}; + +function validateRequest(req: http.IncomingMessage) { + // check if URL + if (!req.url) { + throw new RequestError('Expected request URL', httpStatus.br); + } + + const url = new URL(req.url, 'http://localhost'); + // validate endpoint + const endpoint = url.pathname; + + if (!validatePath(endpoint)) { + throw new RequestError('Not found', httpStatus.nf); + } + + // validate method + const method = req.method; + + if (!validateMethod(method)) { + throw new RequestError(`Unknown method: ${method}`, httpStatus.br); + } + + // check if ep requires searchparam + if (!(endpoint in REQUIRED_PARAMS)) { + return { endpoint, method, param: null }; + } + + const param = url.searchParams.get(REQUIRED_PARAMS[endpoint] as string); + + if (!param) { + throw new RequestError('Missing required token parameter', httpStatus.br); + } + + return { endpoint, method, param }; +} + +export default validateRequest; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..38b45a34 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}