diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..b34fa0b4 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +DATABASE_URL= +PORT= +JWT_SECRET= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4baec517..52db2372 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ dist-ssr *.http HTTP temp +uploads jsconfig.json # Editor directories and files diff --git a/package-lock.json b/package-lock.json index 5ebaea9e..1f204eae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,20 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@prisma/client": "^6.5.0", + "@prisma/client": "^6.7.0", + "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.7", - "express": "^4.21.2" + "express": "^4.21.2", + "express-jwt": "^8.5.1", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.2" }, "devDependencies": { "@types/express": "^5.0.1", "@types/node": "^22.13.10", "nodemon": "^3.1.9", - "prisma": "^6.5.0", + "prisma": "^6.7.0", "tsx": "^4.19.3", "typescript": "^5.8.2" } @@ -448,10 +452,30 @@ "node": ">=18" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/@prisma/client": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.5.0.tgz", - "integrity": "sha512-M6w1Ql/BeiGoZmhMdAZUXHu5sz5HubyVcKukbLs3l0ELcQb8hTUJxtGEChhv4SVJ0QJlwtLnwOLgIRQhpsm9dw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.7.0.tgz", + "integrity": "sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -471,9 +495,9 @@ } }, "node_modules/@prisma/config": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.5.0.tgz", - "integrity": "sha512-sOH/2Go9Zer67DNFLZk6pYOHj+rumSb0VILgltkoxOjYnlLqUpHPAN826vnx8HigqnOCxj9LRhT6U7uLiIIWgw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.7.0.tgz", + "integrity": "sha512-di8QDdvSz7DLUi3OOcCHSwxRNeW7jtGRUD2+Z3SdNE3A+pPiNT8WgUJoUyOwJmUr5t+JA2W15P78C/N+8RXrOA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -482,53 +506,53 @@ } }, "node_modules/@prisma/debug": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.5.0.tgz", - "integrity": "sha512-fc/nusYBlJMzDmDepdUtH9aBsJrda2JNErP9AzuHbgUEQY0/9zQYZdNlXmKoIWENtio+qarPNe/+DQtrX5kMcQ==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.7.0.tgz", + "integrity": "sha512-RabHn9emKoYFsv99RLxvfG2GHzWk2ZI1BuVzqYtmMSIcuGboHY5uFt3Q3boOREM9de6z5s3bQoyKeWnq8Fz22w==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.5.0.tgz", - "integrity": "sha512-FVPQYHgOllJklN9DUyujXvh3hFJCY0NX86sDmBErLvoZjy2OXGiZ5FNf3J/C4/RZZmCypZBYpBKEhx7b7rEsdw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.7.0.tgz", + "integrity": "sha512-3wDMesnOxPrOsq++e5oKV9LmIiEazFTRFZrlULDQ8fxdub5w4NgRBoxtWbvXmj2nJVCnzuz6eFix3OhIqsZ1jw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.5.0", - "@prisma/engines-version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60", - "@prisma/fetch-engine": "6.5.0", - "@prisma/get-platform": "6.5.0" + "@prisma/debug": "6.7.0", + "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", + "@prisma/fetch-engine": "6.7.0", + "@prisma/get-platform": "6.7.0" } }, "node_modules/@prisma/engines-version": { - "version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60.tgz", - "integrity": "sha512-iK3EmiVGFDCmXjSpdsKGNqy9hOdLnvYBrJB61far/oP03hlIxrb04OWmDjNTwtmZ3UZdA5MCvI+f+3k2jPTflQ==", + "version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed.tgz", + "integrity": "sha512-EvpOFEWf1KkJpDsBCrih0kg3HdHuaCnXmMn7XFPObpFTzagK1N0Q0FMnYPsEhvARfANP5Ok11QyoTIRA2hgJTA==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.5.0.tgz", - "integrity": "sha512-3LhYA+FXP6pqY8FLHCjewyE8pGXXJ7BxZw2rhPq+CZAhvflVzq4K8Qly3OrmOkn6wGlz79nyLQdknyCG2HBTuA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.7.0.tgz", + "integrity": "sha512-zLlAGnrkmioPKJR4Yf7NfW3hftcvqeNNEHleMZK9yX7RZSkhmxacAYyfGsCcqRt47jiZ7RKdgE0Wh2fWnm7WsQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.5.0", - "@prisma/engines-version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60", - "@prisma/get-platform": "6.5.0" + "@prisma/debug": "6.7.0", + "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", + "@prisma/get-platform": "6.7.0" } }, "node_modules/@prisma/get-platform": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.5.0.tgz", - "integrity": "sha512-xYcvyJwNMg2eDptBYFqFLUCfgi+wZLcj6HDMsj0Qw0irvauG4IKmkbywnqwok0B+k+W+p+jThM2DKTSmoPCkzw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.7.0.tgz", + "integrity": "sha512-i9IH5lO4fQwnMLvQLYNdgVh9TK3PuWBfQd7QLk/YurnAIg+VeADcZDbmhAi4XBBDD+hDif9hrKyASu0hbjwabw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.5.0" + "@prisma/debug": "6.7.0" } }, "node_modules/@types/body-parser": { @@ -584,6 +608,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -591,11 +625,16 @@ "dev": true, "license": "MIT" }, + "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": "22.13.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -638,6 +677,12 @@ "@types/send": "*" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -651,6 +696,50 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -665,6 +754,32 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -675,9 +790,22 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -719,7 +847,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -739,6 +866,29 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -802,13 +952,81 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -845,6 +1063,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -867,6 +1091,12 @@ "ms": "2.0.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -886,6 +1116,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -912,12 +1151,27 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1097,6 +1351,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-jwt": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.5.1.tgz", + "integrity": "sha512-Dv6QjDLpR2jmdb8M6XQXiCcpEom7mK8TOqnr0/TngDKsG2DHVkO8+XnVxkJVN7BuS1I3OrGw6N8j5DaaGgkDRQ==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9", + "express-unless": "^2.1.3", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-unless": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1146,6 +1420,36 @@ "node": ">= 0.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1170,6 +1474,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1220,6 +1545,27 @@ "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", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1267,6 +1613,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1295,6 +1647,42 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1314,6 +1702,17 @@ "dev": true, "license": "ISC" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1352,6 +1751,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1375,6 +1783,127 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1448,7 +1977,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -1457,12 +1985,97 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1472,6 +2085,32 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/nodemon": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", @@ -1526,6 +2165,21 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1536,6 +2190,19 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1569,6 +2236,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1578,6 +2254,15 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -1598,15 +2283,15 @@ } }, "node_modules/prisma": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.5.0.tgz", - "integrity": "sha512-yUGXmWqv5F4PByMSNbYFxke/WbnyTLjnJ5bKr8fLkcnY7U5rU9rUTh/+Fja+gOrRxEgtCbCtca94IeITj4j/pg==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.7.0.tgz", + "integrity": "sha512-vArg+4UqnQ13CVhc2WUosemwh6hr6cr6FY2uzDvCIFwH8pu8BXVv38PktoMLVjtX7sbYThxbnZF5YiR8sN2clw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "6.5.0", - "@prisma/engines": "6.5.0" + "@prisma/config": "6.7.0", + "@prisma/engines": "6.7.0" }, "bin": { "prisma": "build/index.js" @@ -1626,6 +2311,12 @@ } } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1685,6 +2376,20 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1708,6 +2413,22 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1738,7 +2459,6 @@ "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1801,6 +2521,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -1879,6 +2605,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1901,6 +2633,49 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1914,6 +2689,23 @@ "node": ">=4" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1946,6 +2738,12 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tsx": { "version": "4.19.3", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", @@ -1979,6 +2777,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.8.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", @@ -2004,7 +2808,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -2016,6 +2819,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2033,6 +2842,52 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "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/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" } } } diff --git a/package.json b/package.json index f7537bba..874b4416 100644 --- a/package.json +++ b/package.json @@ -12,21 +12,21 @@ "license": "ISC", "description": "", "dependencies": { - "@prisma/client": "^6.5.0", + "@prisma/client": "^6.7.0", + "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.7", - "express": "^4.21.2" + "express": "^4.21.2", + "express-jwt": "^8.5.1", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.2" }, "devDependencies": { "@types/express": "^5.0.1", "@types/node": "^22.13.10", "nodemon": "^3.1.9", - "prisma": "^6.5.0", + "prisma": "^6.7.0", "tsx": "^4.19.3", "typescript": "^5.8.2" - }, - "prisma": { - "schema": "./src/db/prisma/schema.prisma", - "seed": "node ./src/db/prisma/seed.js" } } diff --git a/src/db/prisma/client.prisma.js b/prisma/client.prisma.js similarity index 100% rename from src/db/prisma/client.prisma.js rename to prisma/client.prisma.js diff --git a/src/db/prisma/migrations/20250318131126_add_first_table/migration.sql b/prisma/migrations/20250318131126_add_first_table/migration.sql similarity index 100% rename from src/db/prisma/migrations/20250318131126_add_first_table/migration.sql rename to prisma/migrations/20250318131126_add_first_table/migration.sql diff --git a/src/db/prisma/migrations/20250318145107_add_product_rel/migration.sql b/prisma/migrations/20250318145107_add_product_rel/migration.sql similarity index 100% rename from src/db/prisma/migrations/20250318145107_add_product_rel/migration.sql rename to prisma/migrations/20250318145107_add_product_rel/migration.sql diff --git a/src/db/prisma/migrations/20250319010554_remove_user_table/migration.sql b/prisma/migrations/20250319010554_remove_user_table/migration.sql similarity index 100% rename from src/db/prisma/migrations/20250319010554_remove_user_table/migration.sql rename to prisma/migrations/20250319010554_remove_user_table/migration.sql diff --git a/src/db/prisma/migrations/20250320094435_add_tag_tags/migration.sql b/prisma/migrations/20250320094435_add_tag_tags/migration.sql similarity index 100% rename from src/db/prisma/migrations/20250320094435_add_tag_tags/migration.sql rename to prisma/migrations/20250320094435_add_tag_tags/migration.sql diff --git a/src/db/prisma/migrations/20250320094633_modify_product_table/migration.sql b/prisma/migrations/20250320094633_modify_product_table/migration.sql similarity index 100% rename from src/db/prisma/migrations/20250320094633_modify_product_table/migration.sql rename to prisma/migrations/20250320094633_modify_product_table/migration.sql diff --git a/src/db/prisma/migrations/20250320171240_add_product_tag_on_delete_cascade/migration.sql b/prisma/migrations/20250320171240_add_product_tag_on_delete_cascade/migration.sql similarity index 100% rename from src/db/prisma/migrations/20250320171240_add_product_tag_on_delete_cascade/migration.sql rename to prisma/migrations/20250320171240_add_product_tag_on_delete_cascade/migration.sql diff --git a/src/db/prisma/migrations/20250321001436_add_tag_name_unique/migration.sql b/prisma/migrations/20250321001436_add_tag_name_unique/migration.sql similarity index 100% rename from src/db/prisma/migrations/20250321001436_add_tag_name_unique/migration.sql rename to prisma/migrations/20250321001436_add_tag_name_unique/migration.sql diff --git a/src/db/prisma/migrations/20250321003748_change_on_delete_attribute/migration.sql b/prisma/migrations/20250321003748_change_on_delete_attribute/migration.sql similarity index 100% rename from src/db/prisma/migrations/20250321003748_change_on_delete_attribute/migration.sql rename to prisma/migrations/20250321003748_change_on_delete_attribute/migration.sql diff --git a/src/db/prisma/migrations/20250321135508_add_user/migration.sql b/prisma/migrations/20250321135508_add_user/migration.sql similarity index 100% rename from src/db/prisma/migrations/20250321135508_add_user/migration.sql rename to prisma/migrations/20250321135508_add_user/migration.sql diff --git a/prisma/migrations/20250417150745_modify_article_table/migration.sql b/prisma/migrations/20250417150745_modify_article_table/migration.sql new file mode 100644 index 00000000..fd63d8f7 --- /dev/null +++ b/prisma/migrations/20250417150745_modify_article_table/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `creatdAt` on the `Article` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Article" DROP COLUMN "creatdAt", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/prisma/migrations/20250418155732_add_on_delete_cascade/migration.sql b/prisma/migrations/20250418155732_add_on_delete_cascade/migration.sql new file mode 100644 index 00000000..06643e54 --- /dev/null +++ b/prisma/migrations/20250418155732_add_on_delete_cascade/migration.sql @@ -0,0 +1,17 @@ +-- DropForeignKey +ALTER TABLE "ArticleComment" DROP CONSTRAINT "ArticleComment_articleId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProductComment" DROP CONSTRAINT "ProductComment_productId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProductLike" DROP CONSTRAINT "ProductLike_productId_fkey"; + +-- AddForeignKey +ALTER TABLE "ProductComment" ADD CONSTRAINT "ProductComment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductLike" ADD CONSTRAINT "ProductLike_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArticleComment" ADD CONSTRAINT "ArticleComment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250509082904_add_user_field/migration.sql b/prisma/migrations/20250509082904_add_user_field/migration.sql new file mode 100644 index 00000000..5c58b607 --- /dev/null +++ b/prisma/migrations/20250509082904_add_user_field/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `encryptedPassword` on the `User` table. All the data in the column will be lost. + - Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "User" DROP COLUMN "encryptedPassword", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "image" TEXT, +ADD COLUMN "password" TEXT NOT NULL, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/prisma/migrations/20250509114208_add_nickname_attribute/migration.sql b/prisma/migrations/20250509114208_add_nickname_attribute/migration.sql new file mode 100644 index 00000000..bb79ad33 --- /dev/null +++ b/prisma/migrations/20250509114208_add_nickname_attribute/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[nickname]` on the table `User` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "User_nickname_key" ON "User"("nickname"); diff --git a/prisma/migrations/20250512040355_add_like_model/migration.sql b/prisma/migrations/20250512040355_add_like_model/migration.sql new file mode 100644 index 00000000..fe4fb065 --- /dev/null +++ b/prisma/migrations/20250512040355_add_like_model/migration.sql @@ -0,0 +1,34 @@ +/* + Warnings: + + - A unique constraint covering the columns `[userId,productId]` on the table `ProductLike` will be added. If there are existing duplicate values, this will fail. + - Added the required column `userId` to the `ProductLike` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "ProductLike" ADD COLUMN "userId" TEXT NOT NULL; + +-- CreateTable +CREATE TABLE "ArticleLike" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + "articleId" INTEGER NOT NULL, + + CONSTRAINT "ArticleLike_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ArticleLike_userId_articleId_key" ON "ArticleLike"("userId", "articleId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProductLike_userId_productId_key" ON "ProductLike"("userId", "productId"); + +-- AddForeignKey +ALTER TABLE "ProductLike" ADD CONSTRAINT "ProductLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArticleLike" ADD CONSTRAINT "ArticleLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArticleLike" ADD CONSTRAINT "ArticleLike_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250513130827_add_product_image_model/migration.sql b/prisma/migrations/20250513130827_add_product_image_model/migration.sql new file mode 100644 index 00000000..3fc7e366 --- /dev/null +++ b/prisma/migrations/20250513130827_add_product_image_model/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "ProductImage" ( + "id" SERIAL NOT NULL, + "imageUrl" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + "productId" INTEGER NOT NULL, + + CONSTRAINT "ProductImage_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ProductImage_imageUrl_key" ON "ProductImage"("imageUrl"); + +-- AddForeignKey +ALTER TABLE "ProductImage" ADD CONSTRAINT "ProductImage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductImage" ADD CONSTRAINT "ProductImage_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250518150827_add_author_field/migration.sql b/prisma/migrations/20250518150827_add_author_field/migration.sql new file mode 100644 index 00000000..e52d584d --- /dev/null +++ b/prisma/migrations/20250518150827_add_author_field/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - Added the required column `authorId` to the `Article` table without a default value. This is not possible if the table is not empty. + - Added the required column `authorId` to the `ArticleComment` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Article" ADD COLUMN "authorId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "ArticleComment" ADD COLUMN "authorId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Product" ADD COLUMN "authorId" TEXT; + +-- AlterTable +ALTER TABLE "ProductComment" ADD COLUMN "authorId" TEXT; + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductComment" ADD CONSTRAINT "ProductComment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Article" ADD CONSTRAINT "Article_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArticleComment" ADD CONSTRAINT "ArticleComment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250518152424_remove_author_optional/migration.sql b/prisma/migrations/20250518152424_remove_author_optional/migration.sql new file mode 100644 index 00000000..b0144c98 --- /dev/null +++ b/prisma/migrations/20250518152424_remove_author_optional/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Made the column `authorId` on table `Product` required. This step will fail if there are existing NULL values in that column. + - Made the column `authorId` on table `ProductComment` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Product" ALTER COLUMN "authorId" SET NOT NULL; + +-- AlterTable +ALTER TABLE "ProductComment" ALTER COLUMN "authorId" SET NOT NULL; diff --git a/prisma/migrations/20250519011947_add_on_delete_cascade/migration.sql b/prisma/migrations/20250519011947_add_on_delete_cascade/migration.sql new file mode 100644 index 00000000..6c46cf33 --- /dev/null +++ b/prisma/migrations/20250519011947_add_on_delete_cascade/migration.sql @@ -0,0 +1,23 @@ +-- DropForeignKey +ALTER TABLE "ArticleLike" DROP CONSTRAINT "ArticleLike_articleId_fkey"; + +-- DropForeignKey +ALTER TABLE "ArticleLike" DROP CONSTRAINT "ArticleLike_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProductImage" DROP CONSTRAINT "ProductImage_productId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProductImage" DROP CONSTRAINT "ProductImage_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "ProductImage" ADD CONSTRAINT "ProductImage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductImage" ADD CONSTRAINT "ProductImage_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArticleLike" ADD CONSTRAINT "ArticleLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArticleLike" ADD CONSTRAINT "ArticleLike_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/db/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml similarity index 81% rename from src/db/prisma/migrations/migration_lock.toml rename to prisma/migrations/migration_lock.toml index 648c57fd..044d57cd 100644 --- a/src/db/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (e.g., Git) -provider = "postgresql" \ No newline at end of file +provider = "postgresql" diff --git a/src/db/prisma/mock/articleMock.js b/prisma/mock/articleMock.js similarity index 91% rename from src/db/prisma/mock/articleMock.js rename to prisma/mock/articleMock.js index 85e5cfe7..9bd7c711 100644 --- a/src/db/prisma/mock/articleMock.js +++ b/prisma/mock/articleMock.js @@ -29,23 +29,23 @@ const ARTICLE_MOCK = [ const ARTICLE_COMMENT_MOCK = [ { content: "댓글 시드 데이터 1", - articleId: 33, + articleId: 1, }, { content: "댓글 시드 데이터 2", - articleId: 34, + articleId: 2, }, { content: "댓글 시드 데이터 3", - articleId: 35, + articleId: 3, }, { content: "댓글 시드 데이터 4", - articleId: 36, + articleId: 4, }, { content: "댓글 시드 데이터 5", - articleId: 37, + articleId: 5, }, ]; diff --git a/src/db/prisma/mock/productMock.js b/prisma/mock/productMock.js similarity index 84% rename from src/db/prisma/mock/productMock.js rename to prisma/mock/productMock.js index 3777e051..17edd197 100644 --- a/src/db/prisma/mock/productMock.js +++ b/prisma/mock/productMock.js @@ -3,31 +3,26 @@ const PRODUCT_MOCK = [ name: "상품 시드 데이터 1", description: "상품 설명 시드 데이터 1", price: 10000, - tags: "상품 태그 시드 데이터 1", }, { name: "상품 시드 데이터 2", description: "상품 설명 시드 데이터 2", price: 20000, - tags: "상품 태그 시드 데이터 2", }, { name: "상품 시드 데이터 3", description: "상품 설명 시드 데이터 3", price: 30000, - tags: "상품 태그 시드 데이터 3", }, { name: "상품 시드 데이터 4", description: "상품 설명 시드 데이터 4", price: 40000, - tags: "상품 태그 시드 데이터 4", }, { name: "상품 시드 데이터 5", description: "상품 설명 시드 데이터 5", price: 50000, - tags: "상품 태그 시드 데이터 5", }, ]; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 00000000..e80ebe52 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,129 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// 사용자 +model User { + id String @id @default(uuid()) + email String @unique + password String + nickname String @unique + image String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + products Product[] + productComments ProductComment[] + productLikes ProductLike[] + articles Article[] + articleComments ArticleComment[] + articleLikes ArticleLike[] + productImagea ProductImage[] +} + +// 상품 +model Product { + id Int @id @default(autoincrement()) + name String + description String + price Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + authorId String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + productLikes ProductLike[] + productComments ProductComment[] + productTags ProductTag[] + productImages ProductImage[] +} + +// 태그 +model Tag { + id Int @id @default(autoincrement()) + name String @unique + productTags ProductTag[] +} + +model ProductTag { + id Int @id @default(autoincrement()) + productId Int + tagId Int + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@unique([productId, tagId]) +} + +// 상품 댓글 +model ProductComment { + id Int @id @default(autoincrement()) + content String + productId Int + createdAt DateTime @default(now()) + authorId String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) +} + +// 상품 좋아요 +model ProductLike { + id Int @id @default(autoincrement()) + userId String + productId Int + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + + @@unique([userId, productId]) +} + +// 상품 이미지 +model ProductImage { + id Int @id @default(autoincrement()) + imageUrl String @unique + userId String + productId Int + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) +} + +// 자유 게시판 +model Article { + id Int @id @default(autoincrement()) + title String + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + authorId String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + articleComments ArticleComment[] + articleLikes ArticleLike[] +} + +// 자유 게시판 댓글 +model ArticleComment { + id Int @id @default(autoincrement()) + content String + createdAt DateTime @default(now()) + articleId Int + authorId String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) +} + +// 자유 게시판 좋아요 +model ArticleLike { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + userId String + articleId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) + + @@unique([userId, articleId]) +} diff --git a/src/db/prisma/seed.js b/prisma/seed.js similarity index 100% rename from src/db/prisma/seed.js rename to prisma/seed.js diff --git a/src/app.js b/src/app.js index 7ae2bdc4..c5e63b38 100644 --- a/src/app.js +++ b/src/app.js @@ -1,7 +1,7 @@ import express from "express"; import cors from "cors"; import "dotenv/config"; -import handleError from "./middlewares/handleErrorMiddleware.js"; +import errorHandler from "./middlewares/errorHandler.js"; import router from "./routes/indexRoutes.js"; const PORT = process.env.PORT || 3000; @@ -17,18 +17,20 @@ app.use( "https://been-panda.vercel.app", "https://been-panda.onrender.com", "http://localhost:3000", + "http://localhost:3001", "http://127.0.0.1:3000", "http://localhost:5173", "http://127.0.0.1:5173", ], }) ); +app.use("/images", express.static("uploads")); // 3. routes 등록 app.use(router); // 4. 에러 미들웨어 등록 -app.use(handleError); +app.use(errorHandler); // 5. 서버 연결 app.listen(PORT, () => { diff --git a/src/controllers/articleController.js b/src/controllers/articleController.js new file mode 100644 index 00000000..c4eda955 --- /dev/null +++ b/src/controllers/articleController.js @@ -0,0 +1,109 @@ +import articleService from "../services/articleService.js"; + +// 게시글 목록 불러오기 +const getArticles = async (req, res, next) => { + try { + const [articles, totalCount] = await articleService.getArticles(req.query); + + res.status(200).json({ list: articles, totalCount }); + } catch (e) { + next(e); + } +}; + +// 게시글 상세조회 +const getArticle = async (req, res, next) => { + const userId = req.auth.id; + const articleId = Number(req.params.articleId); + + try { + const article = await articleService.getArticle(userId, articleId); + + res.status(200).json(article); + } catch (e) { + next(e); + } +}; + +// 게시글 작성 +const createArticle = async (req, res, next) => { + const userId = req.auth.id; + + try { + const newArticle = await articleService.createArticle(userId, req.body); + + res.status(201).json(newArticle); + } catch (e) { + next(e); + } +}; + +// 게시글 수정 +const updateArticle = async (req, res, next) => { + const articleId = Number(req.params.articleId); + + try { + const updatedArticle = await articleService.updateArticle( + articleId, + req.body + ); + + res.status(200).json(updatedArticle); + } catch (e) { + next(e); + } +}; + +// 게시글 삭제 +const deleteArticle = async (req, res, next) => { + const articleId = Number(req.params.articleId); + + try { + await articleService.deleteArticle(articleId); + + res.sendStatus(204); + } catch (e) { + next(e); + } +}; + +// 게시글 좋아요 +const addlikeArticle = async (req, res, next) => { + const userId = req.auth.id; + const articleId = Number(req.params.articleId); + + try { + const like = await articleService.addlikeArticle(userId, articleId); + + res.status(200).json(like); + } catch (e) { + next(e); + } +}; + +// 게시글 좋아요 취소 +const cancelLikeArticle = async (req, res, next) => { + const userId = req.auth.id; + const articleId = Number(req.params.articleId); + + try { + const cancelLike = await articleService.cancelLikeArticle( + userId, + articleId + ); + + res.status(200).json(cancelLike); + } catch (e) { + next(e); + } +}; + +export default { + getArticles, + getArticle, + createArticle, + updateArticle, + deleteArticle, + addlikeArticle, + cancelLikeArticle, +}; diff --git a/src/controllers/productController.js b/src/controllers/productController.js new file mode 100644 index 00000000..c51254d9 --- /dev/null +++ b/src/controllers/productController.js @@ -0,0 +1,141 @@ +import productService from "../services/productService.js"; + +// 상품 목록 불러오기 +const getProducts = async (req, res, next) => { + const baseUrl = `${req.protocol}://${req.get("host")}/images`; + + try { + const [products, totalCount] = await productService.getProducts(req.query); + + const productsWithImages = products.map((product) => ({ + ...product, + productImages: undefined, + images: product.productImages.map((img) => `${baseUrl}/${img.imageUrl}`), + })); + + res.status(200).json({ list: productsWithImages, totalCount }); + } catch (e) { + next(e); + } +}; + +// 상품 상세조회 +const getProduct = async (req, res, next) => { + const userId = req.auth.id; + const productId = Number(req.params.productId); + const baseUrl = `${req.protocol}://${req.get("host")}/images`; + + try { + const product = await productService.getProduct(userId, productId); + + const imageUrls = product.images.map( + (imageUrl) => `${baseUrl}/${imageUrl}` + ); + + res.status(200).json({ ...product, images: imageUrls }); + } catch (e) { + next(e); + } +}; + +// 상품 등록 +const createProduct = async (req, res, next) => { + const userId = req.auth.id; + const images = req.files; + const baseUrl = `${req.protocol}://${req.get("host")}/images`; + + try { + const newProduct = await productService.createProduct( + userId, + req.body, + images + ); + + const imageUrls = newProduct.images.map( + (imageUrl) => `${baseUrl}/${imageUrl}` + ); + + res.status(201).json({ ...newProduct, images: imageUrls }); + } catch (e) { + next(e); + } +}; + +// 상품 수정 +const updateProduct = async (req, res, next) => { + const userId = req.auth.id; + const productId = Number(req.params.productId); + const images = req.files; + const baseUrl = `${req.protocol}://${req.get("host")}/images`; + + try { + const updatedProduct = await productService.updateProduct( + userId, + productId, + req.body, + images + ); + + const imageUrls = updatedProduct.images.map( + (imageUrl) => `${baseUrl}/${imageUrl}` + ); + + res.status(200).json({ ...updatedProduct, images: imageUrls }); + } catch (e) { + next(e); + } +}; + +// 상품 삭제 +const deleteProduct = async (req, res, next) => { + const productId = Number(req.params.productId); + + try { + await productService.deleteProduct(productId); + + res.sendStatus(204); + } catch (e) { + next(e); + } +}; + +// 상품 좋아요 +const addlikeProduct = async (req, res, next) => { + const userId = req.auth.id; + const productId = Number(req.params.productId); + + try { + const like = await productService.addlikeProduct(userId, productId); + + res.status(200).json(like); + } catch (e) { + next(e); + } +}; + +// 상품 좋아요 취소 +const cancelLikeProduct = async (req, res, next) => { + const userId = req.auth.id; + const productId = Number(req.params.productId); + + try { + const cancelLike = await productService.cancelLikeProduct( + userId, + productId + ); + + res.status(200).json(cancelLike); + } catch (e) { + next(e); + } +}; + +export default { + getProducts, + getProduct, + createProduct, + updateProduct, + deleteProduct, + addlikeProduct, + cancelLikeProduct, +}; diff --git a/src/db/prisma/schema.prisma b/src/db/prisma/schema.prisma deleted file mode 100644 index d93f9939..00000000 --- a/src/db/prisma/schema.prisma +++ /dev/null @@ -1,96 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -// 사용자 -model User { - id String @id @default(uuid()) - email String @unique - encryptedPassword String - nickname String - // products Product[] - // productLikes ProductLike[] - // productComments ProductComment[] - // articles Article[] - // articleComments ArticleComment[] -} - -// 상품 -model Product { - id Int @id @default(autoincrement()) - name String - description String - price Int - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - // user User @relation(fields: [userId], references: [id], onDelete: SetNull) - // userId String - productLikes ProductLike[] - productComments ProductComment[] - productTags ProductTag[] -} - -model Tag { - id Int @id @default(autoincrement()) - name String @unique - productTags ProductTag[] -} - -model ProductTag { - id Int @id @default(autoincrement()) - product Product @relation(fields: [productId], references: [id], onDelete: Cascade) - productId Int - tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) - tagId Int - - @@unique([productId, tagId]) -} - -// 상품 댓글 -model ProductComment { - id Int @id @default(autoincrement()) - content String - createdAt DateTime @default(now()) - // author User @relation(fields: [authorId], references: [id], onDelete: Cascade) - // authorId String - product Product @relation(fields: [productId], references: [id]) - productId Int -} - -// 상품 좋아요(중고마켓 & 유저 연결) -model ProductLike { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - // user User @relation(fields: [userId], references: [id], onDelete: SetNull) - // userId String - product Product @relation(fields: [productId], references: [id]) - productId Int -} - -// 자유 게시판 -model Article { - id Int @id @default(autoincrement()) - title String - content String - creatdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - // author User @relation(fields: [authorId], references: [id], onDelete: SetNull) - // authorId String - articleComments ArticleComment[] -} - -// 자유 게시판 댓글 -model ArticleComment { - id Int @id @default(autoincrement()) - content String - createdAt DateTime @default(now()) - // author User @relation(fields: [authorId], references: [id], onDelete: SetNull) - // authorId String - article Article @relation(fields: [articleId], references: [id]) - articleId Int -} diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js new file mode 100644 index 00000000..36872f5a --- /dev/null +++ b/src/middlewares/auth.js @@ -0,0 +1,16 @@ +import { expressjwt } from "express-jwt"; + +const verifyAccessToken = expressjwt({ + secret: process.env.JWT_SECRET, + algorithms: ["HS256"], +}); + +const verifyRefreshToken = expressjwt({ + secret: process.env.JWT_SECRET, + algorithms: ["HS256"], +}); + +export default { + verifyAccessToken, + verifyRefreshToken, +}; diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js new file mode 100644 index 00000000..805c6528 --- /dev/null +++ b/src/middlewares/errorHandler.js @@ -0,0 +1,15 @@ +export default function errorHandler(err, req, res, next) { + const status = err.code ?? 500; + + if (err.name === "UnauthorizedError") { + return res.status(401).json({ message: "토큰이 유효하지 않습니다." }); + } + + return res.status(status).json({ + path: req.path, + method: req.method, + message: err.message ?? "서버 오류입니다.", + data: err.data ?? undefined, + date: new Date(), + }); +} diff --git a/src/middlewares/handleErrorMiddleware.js b/src/middlewares/handleErrorMiddleware.js deleted file mode 100644 index 9a0f5d40..00000000 --- a/src/middlewares/handleErrorMiddleware.js +++ /dev/null @@ -1,25 +0,0 @@ -function handleError(err, req, res, next) { - switch (err.name) { - case "ValidationError": - res.status(400).send({ message: "유효성 검증 실패하였습니다." }); - break; - case "CastError": - res.status(400).send({ message: "잘못된 데이터가 입력되었습니다." }); - break; - case "ReferenceError": - res.status(500).send({ message: "참조할 수 없습니다." }); - break; - default: - res.send({ message: err.message }); - break; - } -} - -// 개발용 에러 미들웨어 -// function handleError(err, req, res, next) { -// const message = err.message; - -// res.send(message); -// } - -export default handleError; diff --git a/src/middlewares/requiredDataValidate.js b/src/middlewares/requiredDataValidate.js new file mode 100644 index 00000000..2c2c34a1 --- /dev/null +++ b/src/middlewares/requiredDataValidate.js @@ -0,0 +1,46 @@ +export default function requiredDataValidate(req, res, next) { + try { + const { name, description, price, tags } = req.body; + + if (!name || !description || !price || !tags) { + const error = new Error("필수 항목을 모두 입력해주세요."); + error.code = 400; + + throw error; + } + + if (10 < name.length) { + const error = new Error("이름은 10글자 이내로 입력해주세요."); + error.code = 400; + + throw error; + } + + if (10 > description.length || 100 < description.length) { + const error = new Error("설명은 10 ~ 100글자 이내로 입력해주세요."); + error.code = 400; + + throw error; + } + + if (typeof Number(price) !== "number") { + const error = new Error("가격은 숫자만 입력해주세요."); + error.code = 400; + + throw error; + } + + JSON.parse(tags).map((tag) => { + if (Boolean(5 < tag.length)) { + const error = new Error("태그는 5글자 이내로 입력해주세요."); + error.code = 400; + + throw error; + } + }); + + next(); + } catch (e) { + next(e); + } +} diff --git a/src/repositories/articleRepository.js b/src/repositories/articleRepository.js new file mode 100644 index 00000000..c8f0e48f --- /dev/null +++ b/src/repositories/articleRepository.js @@ -0,0 +1,98 @@ +import prisma from "../../prisma/client.prisma.js"; + +const findAll = (query) => { + const { offset, limit, orderBy, keyword } = query; + const filter = { + OR: [ + { title: { contains: keyword || "", mode: "insensitive" } }, + { content: { contains: keyword || "", mode: "insensitive" } }, + ], + }; + const orderByCondition = + orderBy === "recent" + ? { createdAt: "desc" } + : { articleLikes: { _count: "desc" } }; + + return Promise.all([ + prisma.article.findMany({ + where: filter, + skip: (Number(offset) - 1) * Number(limit) || 0, + take: Number(limit) || 10, + orderBy: orderByCondition, + omit: { updatedAt: true, authorId: true }, + include: { author: { select: { nickname: true } } }, + }), + prisma.article.count({ where: filter }), + ]); +}; + +const findArticleLikeCountById = (articleId) => { + return prisma.articleLike.count({ where: { articleId } }); +}; + +const findById = (userId, articleId) => { + return Promise.all([ + prisma.article.findUnique({ + where: { id: articleId }, + omit: { updatedAt: true, authorId: true }, + include: { author: { select: { id: true, nickname: true } } }, + }), + prisma.articleLike.count({ + where: { articleId }, + }), + prisma.articleLike.findUnique({ + where: { userId_articleId: { userId, articleId } }, + }), + ]); +}; + +const findByIdWithTx = (tx, articleId) => { + return tx.article.findUnique({ + where: { id: articleId }, + }); +}; + +const create = (userId, body) => { + const { title, content } = body; + + return prisma.article.create({ + data: { title, content, authorId: userId }, + }); +}; + +const updateWithTx = (tx, articleId, body) => { + const { title, content } = body; + + return tx.article.update({ + where: { id: articleId }, + data: { title, content }, + }); +}; + +const deleteWithTx = (tx, articleId) => { + return tx.article.delete({ + where: { id: articleId }, + }); +}; + +const addlikeArticle = (userId, articleId) => { + return prisma.articleLike.create({ data: { userId, articleId } }); +}; + +const cancelLikeArticle = (userId, articleId) => { + return prisma.articleLike.delete({ + where: { userId_articleId: { userId, articleId } }, + }); +}; + +export default { + findAll, + findArticleLikeCountById, + findById, + findByIdWithTx, + create, + updateWithTx, + deleteWithTx, + addlikeArticle, + cancelLikeArticle, +}; diff --git a/src/repositories/productRepository.js b/src/repositories/productRepository.js new file mode 100644 index 00000000..6bec434d --- /dev/null +++ b/src/repositories/productRepository.js @@ -0,0 +1,139 @@ +import prisma from "../../prisma/client.prisma.js"; + +const findAll = (query) => { + const { offset, limit, orderBy, keyword } = query; + const filter = { + OR: [ + { name: { contains: keyword || "", mode: "insensitive" } }, + { description: { contains: keyword || "", mode: "insensitive" } }, + ], + }; + const orderByCondition = + orderBy === "recent" + ? { createdAt: "desc" } + : { productLikes: { _count: "desc" } }; + + return Promise.all([ + prisma.product.findMany({ + where: filter, + skip: (Number(offset) - 1) * Number(limit) || 0, + take: Number(limit) || 10, + orderBy: orderByCondition, + omit: { description: true, authorId: true, updatedAt: true }, + include: { productImages: { select: { imageUrl: true } } }, + }), + prisma.product.count({ where: filter }), + ]); +}; + +const findProductLikeCountById = (productId) => { + return prisma.productLike.count({ where: { productId } }); +}; + +const findByIdWithTx = (tx, userId, productId) => { + return Promise.all([ + tx.product.findUnique({ + where: { id: productId }, + omit: { updatedAt: true, authorId: true }, + include: { author: { select: { id: true, nickname: true } } }, + }), + tx.productImage.findMany({ + where: { productId }, + }), + tx.productLike.count({ + where: { productId }, + }), + tx.productLike.findUnique({ + where: { userId_productId: { userId, productId } }, + }), + ]); +}; + +const findOnlyProductByIdWithTx = (tx, productId) => { + return tx.product.findUnique({ where: { id: productId } }); +}; + +const findProductTagByIdWithTx = (tx, productId) => { + return tx.productTag.findMany({ + where: { productId }, + include: { tag: true }, + }); +}; + +const createWithTx = (tx, userId, body) => { + const { name, description, price } = body; + + return tx.product.create({ + data: { name, description, price: Number(price), authorId: userId }, + }); +}; + +const createProductImageWithTx = (tx, imageUrl = "", userId, productId) => { + return tx.productImage.create({ + data: { imageUrl, userId, productId }, + }); +}; + +const deleteProductImageWithTx = (tx, productId) => { + return tx.productImage.deleteMany({ + where: { productId }, + }); +}; + +const findTagByNameWithTx = (tx, tagName) => { + return tx.tag.findUnique({ where: { name: tagName } }); +}; + +const createTagWithTx = (tx, tagName) => { + return tx.tag.create({ data: { name: tagName } }); +}; + +const createProductTagWithTx = (tx, productId, tagId) => { + return tx.productTag.create({ data: { productId, tagId } }); +}; + +const updateProductWithTx = (tx, productId, body) => { + const { name, description, price } = body; + + return tx.product.update({ + where: { id: productId }, + data: { name, description, price: Number(price) }, + }); +}; + +const deleteProductTagsWithTx = (tx, productId) => { + return tx.productTag.deleteMany({ where: { productId } }); +}; + +const deleteProductWithTx = (tx, productId) => { + return tx.product.delete({ where: { id: productId } }); +}; + +const addlikeProduct = (userId, productId) => { + return prisma.productLike.create({ data: { userId, productId } }); +}; + +const cancelLikeProduct = (userId, productId) => { + return prisma.productLike.delete({ + where: { userId_productId: { userId, productId } }, + }); +}; + +export default { + findAll, + findProductLikeCountById, + findByIdWithTx, + findOnlyProductByIdWithTx, + findProductTagByIdWithTx, + createWithTx, + createProductImageWithTx, + findTagByNameWithTx, + createTagWithTx, + createProductTagWithTx, + updateProductWithTx, + deleteProductImageWithTx, + deleteProductTagsWithTx, + deleteProductWithTx, + addlikeProduct, + cancelLikeProduct, +}; diff --git a/src/routes/articleRoutes.js b/src/routes/articleRoutes.js index 785deb8c..44403594 100644 --- a/src/routes/articleRoutes.js +++ b/src/routes/articleRoutes.js @@ -1,108 +1,52 @@ import express from "express"; -import prisma from "../db/prisma/client.prisma.js"; +import articleController from "../controllers/articleController.js"; +import auth from "../middlewares/auth.js"; const articleRouter = express.Router(); // 게시글 목록 불러오기 -articleRouter.get("/", async (req, res, next) => { - try { - const { offset, limit, orderBy, keyword } = req.query; - const filter = { - OR: [ - { title: { contains: keyword || "", mode: "insensitive" } }, - { content: { contains: keyword || "", mode: "insensitive" } }, - ], - }; - - const articles = await prisma.article.findMany({ - where: filter, - skip: (Number(offset) - 1) * Number(limit) || 0, - take: Number(limit) || 10, - orderBy: { creatdAt: orderBy === "recent" ? "desc" : "asc" }, - omit: { updatedAt: true }, - }); - - const totalCount = await prisma.article.count({ where: filter }); - - res.json({ list: articles, totalCount }); - } catch (e) { - next(e); - } -}); +articleRouter.get("/", auth.verifyAccessToken, articleController.getArticles); // 게시글 상세조회 -articleRouter.get("/:articleId", async (req, res, next) => { - try { - const articleId = Number(req.params.articleId); - - const article = await prisma.article.findUnique({ - where: { id: articleId }, - omit: { updatedAt: true }, - }); - if (!article) throw new Error("해당 게시글을 찾을 수 없습니다."); - - res.json(article); - } catch (e) { - next(e); - } -}); +articleRouter.get( + "/:articleId", + auth.verifyAccessToken, + articleController.getArticle +); // 게시글 작성 -articleRouter.post("/", async (req, res, next) => { - try { - const { title, content } = req.body; - if (!title) throw new Error("제목을 입력해주세요."); - if (!content) throw new Error("내용을 입력해주세요."); - - const newArticle = await prisma.article.create({ - data: { title, content }, - }); - - res.status(201).json(newArticle); - } catch (e) { - next(e); - } -}); +articleRouter.post( + "/", + auth.verifyAccessToken, + articleController.createArticle +); // 게시글 수정 -articleRouter.patch("/:articleId", async (req, res, next) => { - try { - const { title, content } = req.body; - const articleId = Number(req.params.articleId); - if (!(title || content)) throw new Error("수정할 내용을 입력해주세요."); - - await prisma.$transaction(async (tx) => { - const article = await tx.article.findUnique({ where: { id: articleId } }); - if (!article) throw new Error("게시글을 찾을 수 없습니다."); - - const updateArticle = await tx.article.update({ - where: { id: articleId }, - data: { title, content }, - }); - - res.status(200).json(updateArticle); - }); - } catch (e) { - next(e); - } -}); +articleRouter.patch( + "/:articleId", + auth.verifyAccessToken, + articleController.updateArticle +); // 게시글 삭제 -articleRouter.delete("/:articleId", async (req, res, next) => { - try { - const articleId = Number(req.params.articleId); - - await prisma.$transaction(async (tx) => { - const article = await tx.article.findUnique({ where: { id: articleId } }); - if (!article) throw new Error("이미 삭제된 게시글 입니다."); - - await prisma.article.delete({ where: { id: articleId } }); - - res.sendStatus(204); - }); - } catch (e) { - next(e); - } -}); +articleRouter.delete( + "/:articleId", + auth.verifyAccessToken, + articleController.deleteArticle +); + +// 게시글 좋아요 +articleRouter.post( + "/:articleId/like", + auth.verifyAccessToken, + articleController.addlikeArticle +); + +// 게시글 좋아요 취소 +articleRouter.delete( + "/:articleId/like", + auth.verifyAccessToken, + articleController.cancelLikeArticle +); export default articleRouter; diff --git a/src/routes/commentRoutes.js b/src/routes/commentRoutes.js index 883c9ad6..b3e0461a 100644 --- a/src/routes/commentRoutes.js +++ b/src/routes/commentRoutes.js @@ -1,5 +1,6 @@ import express from "express"; -import prisma from "../db/prisma/client.prisma.js"; +import prisma from "../../prisma/client.prisma.js"; +import auth from "../middlewares/auth.js"; const commentRouter = express.Router(); @@ -7,78 +8,94 @@ const ARTICLE_COMMENT = "/articles/:articleId/comments"; const PRODUCT_COMMENT = "/products/:productId/comments"; // 게시글 댓글 불러오기 -commentRouter.get(`${ARTICLE_COMMENT}`, async (req, res, next) => { - try { - const articleId = Number(req.params.articleId); - const { limit, cursor } = req.query; +commentRouter.get( + `${ARTICLE_COMMENT}`, + auth.verifyAccessToken, + async (req, res, next) => { + try { + const articleId = Number(req.params.articleId); + const { limit, cursor } = req.query; - await prisma.$transaction(async (tx) => { - const articleCommentId = cursor - ? await tx.articleComment.findFirst({ + await prisma.$transaction(async (tx) => { + const articleCommentId = + cursor && + (await tx.articleComment.findFirst({ where: { articleId, id: Number(cursor) }, - }) - : false; - - const articleComment = await tx.articleComment.findMany({ - where: { articleId }, - skip: articleCommentId ? 1 : undefined, - take: Number(limit) || 10, - cursor: articleCommentId ? { id: Number(cursor) } : undefined, - omit: { articleId: true }, - }); + })); + + const articleComment = await tx.articleComment.findMany({ + where: { articleId }, + skip: articleCommentId ? 1 : undefined, + take: Number(limit) || 10, + cursor: articleCommentId ? { id: Number(cursor) } : undefined, + omit: { articleId: true, authorId: true }, + include: { author: { select: { id: true, nickname: true } } }, + }); - res.json(articleComment); - }); - } catch (e) { - next(e); + res.json(articleComment); + }); + } catch (e) { + next(e); + } } -}); +); // 게시글 댓글 작성 -commentRouter.post(`${ARTICLE_COMMENT}`, async (req, res, next) => { - try { - const articleId = Number(req.params.articleId); +commentRouter.post( + `${ARTICLE_COMMENT}`, + auth.verifyAccessToken, + async (req, res, next) => { + const userId = req.auth.id; + + try { + const articleId = Number(req.params.articleId); - const { content } = req.body; + const { content } = req.body; - const newArticleComment = await prisma.articleComment.create({ - data: { content, articleId }, - }); + const newArticleComment = await prisma.articleComment.create({ + data: { authorId: userId, articleId, content }, + }); - res.status(201).json(newArticleComment); - } catch (e) { - next(e); + res.status(201).json(newArticleComment); + } catch (e) { + next(e); + } } -}); +); // 게시글 댓글 수정 -commentRouter.patch(`${ARTICLE_COMMENT}/:commentId`, async (req, res, next) => { - try { - const articleId = Number(req.params.articleId); - const commentId = Number(req.params.commentId); - const { content } = req.body; +commentRouter.patch( + `${ARTICLE_COMMENT}/:commentId`, + auth.verifyAccessToken, + async (req, res, next) => { + try { + const articleId = Number(req.params.articleId); + const commentId = Number(req.params.commentId); + const { content } = req.body; - await prisma.$transaction(async (tx) => { - const articleComment = await tx.articleComment.findUnique({ - where: { articleId, id: commentId }, - }); - if (!articleComment) throw new Error("존재하지 않는 댓글입니다."); + await prisma.$transaction(async (tx) => { + const articleComment = await tx.articleComment.findUnique({ + where: { articleId, id: commentId }, + }); + if (!articleComment) throw new Error("존재하지 않는 댓글입니다."); - const updateArticleComment = await tx.articleComment.update({ - where: { articleId, id: commentId }, - data: { content }, - }); + const updateArticleComment = await tx.articleComment.update({ + where: { articleId, id: commentId }, + data: { content }, + }); - res.status(200).json(updateArticleComment); - }); - } catch (e) { - next(e); + res.status(200).json(updateArticleComment); + }); + } catch (e) { + next(e); + } } -}); +); // 게시글 댓글 삭제 commentRouter.delete( `${ARTICLE_COMMENT}/:commentId`, + auth.verifyAccessToken, async (req, res, next) => { const articleId = Number(req.params.articleId); const commentId = Number(req.params.commentId); @@ -104,46 +121,116 @@ commentRouter.delete( ); // 상품 댓글 불러오기 -commentRouter.get(`${PRODUCT_COMMENT}`, async (req, res, next) => { - try { - const productId = Number(req.params.productId); - const { limit, cursor } = req.query; +commentRouter.get( + `${PRODUCT_COMMENT}`, + auth.verifyAccessToken, + async (req, res, next) => { + try { + const productId = Number(req.params.productId); + const { limit, cursor } = req.query; - await prisma.$transaction(async (tx) => { - const productCommentId = await tx.productComment.findFirst({ - where: { productId, id: Number(cursor) }, + await prisma.$transaction(async (tx) => { + const productCommentId = cursor + ? await tx.productComment.findFirst({ + where: { productId, id: Number(cursor) }, + }) + : false; + + const productComment = await tx.productComment.findMany({ + where: { productId }, + skip: productCommentId ? 1 : undefined, + take: Number(limit) || 10, + cursor: productCommentId ? { id: Number(cursor) } : undefined, + omit: { productId: true, authorId: true }, + include: { author: { select: { id: true, nickname: true } } }, + }); + + res.json(productComment); }); + } catch (e) { + next(e); + } + } +); + +// 상품 댓글 작성 +commentRouter.post( + `${PRODUCT_COMMENT}`, + auth.verifyAccessToken, + async (req, res, next) => { + const userId = req.auth.id; - const productComment = await tx.productComment.findMany({ - where: { productId }, - skip: productCommentId ? 1 : undefined, - take: Number(limit) || 10, - cursor: productCommentId ? { id: Number(cursor) } : undefined, - omit: { productId: true }, + try { + const productId = Number(req.params.productId); + + const { content } = req.body; + + const newProductComment = await prisma.productComment.create({ + data: { authorId: userId, productId, content }, }); - res.json(productComment); - }); - } catch (e) { - next(e); + res.status(201).json(newProductComment); + } catch (e) { + next(e); + } } -}); +); -// 상품 댓글 작성 -commentRouter.post(`${PRODUCT_COMMENT}`, async (req, res, next) => { - try { - const productId = Number(req.params.productId); +// 상품 댓글 수정 +commentRouter.patch( + `${PRODUCT_COMMENT}/:commentId`, + auth.verifyAccessToken, + async (req, res, next) => { + try { + const productId = Number(req.params.productId); + const commentId = Number(req.params.commentId); + const { content } = req.body; - const { content } = req.body; + await prisma.$transaction(async (tx) => { + const productComment = await tx.productComment.findUnique({ + where: { productId, id: commentId }, + }); + if (!productComment) throw new Error("존재하지 않는 댓글입니다."); - const newProductComment = await prisma.productComment.create({ - data: { content, productId }, - }); + const updateProductComment = await tx.productComment.update({ + where: { productId, id: commentId }, + data: { content }, + }); - res.status(201).json(newProductComment); - } catch (e) { - next(e); + res.status(200).json(updateProductComment); + }); + } catch (e) { + next(e); + } + } +); + +// 상품 댓글 삭제 +commentRouter.delete( + `${PRODUCT_COMMENT}/:commentId`, + auth.verifyAccessToken, + async (req, res, next) => { + const productId = Number(req.params.productId); + const commentId = Number(req.params.commentId); + try { + await prisma.$transaction(async (tx) => { + const productComment = await tx.productComment.findUnique({ + where: { productId, id: commentId }, + }); + if (!productComment) { + throw new Error("이미 삭제된 댓글입니다."); + } + + await tx.productComment.delete({ + where: { productId, id: commentId }, + }); + + res.sendStatus(204); + }); + } catch (e) { + next(e); + } } -}); +); export default commentRouter; diff --git a/src/routes/indexRoutes.js b/src/routes/indexRoutes.js index 5e085c05..b9308968 100644 --- a/src/routes/indexRoutes.js +++ b/src/routes/indexRoutes.js @@ -6,7 +6,7 @@ import commentRouter from "./commentRoutes.js"; const router = express.Router(); -router.use("/users", userRouter); +router.use("/auth", userRouter); router.use("/products", productRouter); router.use("/articles", articleRouter); router.use("/", commentRouter); diff --git a/src/routes/productRoutes.js b/src/routes/productRoutes.js index c26c9b32..6fb721a2 100644 --- a/src/routes/productRoutes.js +++ b/src/routes/productRoutes.js @@ -1,147 +1,53 @@ import express from "express"; -import prisma from "../db/prisma/client.prisma.js"; +import requiredDataValidate from "../middlewares/requiredDataValidate.js"; +import productController from "../controllers/productController.js"; +import multer from "multer"; +import auth from "../middlewares/auth.js"; const productRouter = express.Router(); -// 상품 목록 불러오기 -productRouter.get("/", async (req, res, next) => { - try { - const { offset, limit, orderBy, keyword } = req.query; - const filter = { - OR: [ - { name: { contains: keyword || "", mode: "insensitive" } }, - { description: { contains: keyword || "", mode: "insensitive" } }, - ], - }; - - await prisma.$transaction(async (tx) => { - const products = await tx.product.findMany({ - where: filter, - skip: (Number(offset) - 1) * Number(limit) || 0, - take: Number(limit) || 10, - orderBy: { createdAt: orderBy === "recent" ? "desc" : "asc" }, - omit: { description: true, updatedAt: true }, - }); - - const totalCount = await tx.product.count({ where: filter }); +// 이미지 업로드 +const upload = multer({ dest: "uploads/" }); - res.json({ list: products, totalCount }); - }); - } catch (e) { - next(e); - } -}); +// 상품 목록 불러오기 +productRouter.get("/", auth.verifyAccessToken, productController.getProducts); // 상품 상세조회 -productRouter.get("/:productId", async (req, res, next) => { - try { - const productId = Number(req.params.productId); - - await prisma.$transaction(async (tx) => { - const product = await tx.product.findUnique({ - where: { id: productId }, - omit: { updatedAt: true }, - }); - if (!product) throw new Error("상품을 찾을 수 없습니다."); - - const productTag = await tx.productTag.findMany({ - where: { productId }, - include: { tag: true }, - }); - - const tags = productTag.map((tag) => tag.tag.name); - - res.json({ ...product, tags }); - }); - } catch (e) { - next(e); - } -}); +productRouter.get( + "/:productId", + auth.verifyAccessToken, + productController.getProduct +); // 상품 등록 -productRouter.post("/", async (req, res, next) => { - try { - const { name, description, price, tags } = req.body; - if (10 < name.length) throw new Error("10글자 이내로 입력해주세요."); - if (10 > description.length || 100 < description.length) - throw new Error("10 ~ 100글자 이내로 입력해주세요."); - tags.map((tag) => { - if (Boolean(5 < tag.length)) - throw new Error("5글자 이내로 입력해주세요."); - }); - - await prisma.$transaction(async (tx) => { - const newProduct = await tx.product.create({ - data: { name, description, price }, - }); - - const newTags = await Promise.all( - tags.map(async (tagName) => { - let tag = await tx.tag.findUnique({ where: { name: tagName } }); - if (!tag) tag = await tx.tag.create({ data: { name: tagName } }); - - await tx.productTag.create({ - data: { productId: newProduct.id, tagId: tag.id }, - }); - return tag.name; - }) - ); - - res.status(201).json({ ...newProduct, tags: newTags }); - }); - } catch (e) { - next(e); - } -}); +productRouter.post( + "/", + auth.verifyAccessToken, + upload.array("imageFiles", 3), + requiredDataValidate, + productController.createProduct +); // 상품 수정 -productRouter.patch("/:productId", async (req, res, next) => { - try { - const { name, description, price, tags } = req.body; - const productId = Number(req.params.productId); - if (!(name || description || price || tags)) - throw new Error("수정할 내용을 입력해주세요."); - - await prisma.$transaction(async (tx) => { - const updateProduct = await tx.product.update({ - where: { id: productId }, - data: { name, description, price }, - }); - - await tx.productTag.deleteMany({ where: { productId } }); - - const updateTags = await Promise.all( - tags.map(async (tagName) => { - let tag = await tx.tag.findUnique({ where: { name: tagName } }); - if (!tag) tag = await tx.tag.create({ data: { name: tagName } }); - await tx.productTag.create({ data: { productId, tagId: tag.id } }); - return tag.name; - }) - ); - - res.status(200).json({ ...updateProduct, tags: updateTags }); - }); - } catch (e) { - next(e); - } -}); +productRouter.patch( + "/:productId", + auth.verifyAccessToken, + upload.array("imageFiles", 3), + requiredDataValidate, + productController.updateProduct +); // 상품 삭제 -productRouter.delete("/:productId", async (req, res, next) => { - try { - const productId = Number(req.params.productId); - - await prisma.$transaction(async (tx) => { - const product = await tx.product.findUnique({ where: { id: productId } }); - if (!product) throw new Error("이미 삭제된 상품입니다."); - - await tx.product.delete({ where: { id: productId } }); - - res.sendStatus(204); - }); - } catch (e) { - next(e); - } -}); +productRouter.delete( + "/:productId", + auth.verifyAccessToken, + productController.deleteProduct +); + +// 상품 좋아요 & 좋아요 취소 +productRouter + .route("/:productId/like") + .post(auth.verifyAccessToken, productController.addlikeProduct) + .delete(auth.verifyAccessToken, productController.cancelLikeProduct); export default productRouter; diff --git a/src/routes/userRoutes.js b/src/routes/userRoutes.js index 7854d97e..f6970f20 100644 --- a/src/routes/userRoutes.js +++ b/src/routes/userRoutes.js @@ -1,18 +1,74 @@ import express from "express"; -import prisma from "../db/prisma/client.prisma.js"; +import prisma from "../../prisma/client.prisma.js"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import auth from "../middlewares/auth.js"; const userRouter = express.Router(); +// 비밀번호 제외 +function filterPassword(user) { + const { password, ...data } = user; + + return data; +} + +// JWT 토큰 발급 +function createToken(user, type = "accessToken") { + const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { + expiresIn: type === "accessToken" ? "30m" : "1day", + }); + + return token; +} + // 회원가입 -userRouter.post("/signup", async (req, res, next) => { +userRouter.post("/signUp", async (req, res, next) => { try { const { email, password, nickname } = req.body; + // 유효성 검사 실패 + if (!email || !password || !nickname) { + const error = new Error("이메일, 비밀번호, 닉네임을 모두 입력해주세요."); + error.code = 400; + + throw error; + } + + const existedUser = await prisma.user.findUnique({ + where: { email }, + }); + + // 존재하는 이메일 + if (existedUser) { + const error = new Error("이미 존재하는 이메일입니다."); + error.code = 400; + + throw error; + } + + const existedNickname = await prisma.user.findUnique({ + where: { nickname }, + }); + + // 존재하는 닉네임 + if (existedNickname) { + const error = new Error("이미 존재하는 닉네임입니다."); + error.code = 400; + + throw error; + } + + const encryptedPassword = await bcrypt.hash(password, 10); const user = await prisma.user.create({ - data: { email, encryptedPassword: password, nickname }, + data: { email, password: encryptedPassword, nickname }, }); - res.status(201).json(user); + const filterPasswordUser = filterPassword(user); + const accessToken = createToken(user); + const refreshToken = createToken(user, "refreshToken"); + + res.status(201).json({ ...filterPasswordUser, accessToken, refreshToken }); } catch (e) { next(e); } @@ -23,19 +79,78 @@ userRouter.post("/login", async (req, res, next) => { try { const { email, password } = req.body; - await prisma.$transaction(async (tx) => { - const user = await tx.user.findUnique({ - where: { email, encryptedPassword: password }, - }); - if (!user) throw new Error("존재하지 않는 사용자입니다."); + const user = await prisma.user.findUnique({ + where: { email }, + }); + + // 이메일 불일치 + if (!user) { + const error = new Error("존재하지 않는 이메일입니다."); + error.code = 400; + + throw error; + } - const token = { accessToken: `@${user.id}@` }; + // 비밀번호 불일치 + const verifyPassword = await bcrypt.compare(password, user.password); - res.status(200).json(token); + if (!verifyPassword) { + const error = new Error("비밀번호가 일치하지 않습니다."); + error.code = 400; + + throw error; + } + + const filterPasswordUser = filterPassword(user); + const accessToken = createToken(user); + const refreshToken = createToken(user, "refreshToken"); + + res.status(200).json({ ...filterPasswordUser, accessToken, refreshToken }); + } catch (e) { + next(e); + } +}); + +// 유저 정보 불러오기 +userRouter.get("/me", auth.verifyAccessToken, async (req, res, next) => { + const userId = req.auth.id; + + try { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { id: true, nickname: true, image: true }, }); + + res.status(200).json(user); } catch (e) { next(e); } }); +// 액세스 토큰 재발급 +userRouter.post( + "/refresh-token", + auth.verifyRefreshToken, + async (req, res, next) => { + const userId = req.auth.id; + try { + const user = await prisma.user.findUnique({ where: { id: userId } }); + + if (!user) { + const error = new Error("인증에 실패하였습니다."); + error.code = 401; + + throw error; + } + + const accessToken = createToken(user); + const refreshToken = createToken(user, "refreshToken"); + + res.status(200).json({ accessToken, refreshToken }); + } catch (e) { + next(e); + } + } +); + export default userRouter; diff --git a/src/services/articleService.js b/src/services/articleService.js new file mode 100644 index 00000000..23ffebce --- /dev/null +++ b/src/services/articleService.js @@ -0,0 +1,117 @@ +import prisma from "../../prisma/client.prisma.js"; +import articleRepository from "../repositories/articleRepository.js"; + +// 게시글 목록 불러오기 +const getArticles = async (query) => { + const [articles, totalCount] = await articleRepository.findAll(query); + + if (!articles || articles.length === 0) { + const error = new Error("게시글이 없습니다."); + error.code = 404; + + throw error; + } + + const articletWithLikeCount = await Promise.all( + articles.map(async (article) => { + const likeCount = await articleRepository.findArticleLikeCountById( + article.id + ); + + return { ...article, likeCount }; + }) + ); + + return [articletWithLikeCount, totalCount]; +}; + +// 게시글 상세조회 +const getArticle = async (userId, articleId) => { + const [article, likeCount, isLiked] = await articleRepository.findById( + userId, + articleId + ); + + if (!article) { + const error = new Error("해당 게시글을 찾을 수 없습니다."); + error.code = 404; + + throw error; + } + + return { ...article, likeCount, isLiked: !!isLiked }; +}; + +// 게시글 작성 +const createArticle = (userId, body) => { + const { title, content } = body; + + if (!title || !content) { + const error = new Error("필수 항목을 모두 입력해주세요."); + error.code = 400; + + throw error; + } + + return articleRepository.create(userId, body); +}; + +// 게시글 수정 +const updateArticle = async (articleId, body) => { + const { title, content } = body; + + if (!(title || content)) { + const error = new Error("수정할 내용을 입력해주세요."); + error.code = 400; + + throw error; + } + + return await prisma.$transaction(async (tx) => { + const article = await articleRepository.findByIdWithTx(tx, articleId); + + if (!article) { + const error = new Error("해당 게시글을 찾을 수 없습니다."); + error.code = 404; + + throw error; + } + + return articleRepository.updateWithTx(tx, articleId, body); + }); +}; + +// 게시글 삭제 +const deleteArticle = async (articleId) => { + return await prisma.$transaction(async (tx) => { + const article = await articleRepository.findByIdWithTx(tx, articleId); + + if (!article) { + const error = new Error("이미 삭제된 게시글입니다."); + error.code = 404; + + throw error; + } + + return articleRepository.deleteWithTx(tx, articleId); + }); +}; + +// 게시글 좋아요 +const addlikeArticle = (userId, articleId) => { + return articleRepository.addlikeArticle(userId, articleId); +}; + +// 게시글 좋아요 취소 +const cancelLikeArticle = (userId, articleId) => { + return articleRepository.cancelLikeArticle(userId, articleId); +}; +export default { + getArticles, + getArticle, + createArticle, + updateArticle, + deleteArticle, + addlikeArticle, + cancelLikeArticle, +}; diff --git a/src/services/productService.js b/src/services/productService.js new file mode 100644 index 00000000..833b9053 --- /dev/null +++ b/src/services/productService.js @@ -0,0 +1,184 @@ +import prisma from "../../prisma/client.prisma.js"; +import productRepository from "../repositories/productRepository.js"; + +// 상품 목록 불러오기 +const getProducts = async (query) => { + const [products, totalCount] = await productRepository.findAll(query); + + if (!products || products.length === 0) { + const error = new Error("상품이 없습니다."); + error.code = 404; + + throw error; + } + + const productWithLikeCount = await Promise.all( + products.map(async (product) => { + const likeCount = await productRepository.findProductLikeCountById( + product.id + ); + + return { ...product, likeCount }; + }) + ); + + return [productWithLikeCount, totalCount]; +}; + +// 상품 상세조회 +const getProduct = async (userId, productId) => { + return await prisma.$transaction(async (tx) => { + const [product, images, likeCount, isLiked] = + await productRepository.findByIdWithTx(tx, userId, productId); + + if (!product) { + const error = new Error("존재하지 않는 상품입니다."); + error.code = 404; + + throw error; + } + + const productTags = await productRepository.findProductTagByIdWithTx( + tx, + productId + ); + + const tags = productTags.map((tag) => tag.tag.name); + const imageUrls = images.map((image) => image.imageUrl); + + return { + ...product, + tags, + images: imageUrls, + likeCount, + isLiked: !!isLiked, + }; + }); +}; + +// 상품 등록 +const createProduct = async (userId, body, images) => { + const { tags } = body; + + return await prisma.$transaction(async (tx) => { + const newProduct = await productRepository.createWithTx(tx, userId, body); + + const newTags = await Promise.all( + JSON.parse(tags).map(async (tagName) => { + let tag = await productRepository.findTagByNameWithTx(tx, tagName); + + if (!tag) { + tag = await productRepository.createTagWithTx(tx, tagName); + } + + await productRepository.createProductTagWithTx( + tx, + newProduct.id, + tag.id + ); + + return tag.name; + }) + ); + + const newImages = await Promise.all( + images.map(async (image) => { + const newImage = await productRepository.createProductImageWithTx( + tx, + image.filename, + userId, + newProduct.id + ); + + return newImage.imageUrl; + }) + ); + + return { ...newProduct, tags: newTags, images: newImages }; + }); +}; + +// 상품 수정 +const updateProduct = async (userId, productId, body, images) => { + const { tags } = body; + + return await prisma.$transaction(async (tx) => { + const updatedProduct = await productRepository.updateProductWithTx( + tx, + productId, + body + ); + + await productRepository.deleteProductTagsWithTx(tx, productId); + + const updatedTags = await Promise.all( + JSON.parse(tags).map(async (tagName) => { + let tag = await productRepository.findTagByNameWithTx(tx, tagName); + + if (!tag) { + tag = await productRepository.createTagWithTx(tx, tagName); + } + + await productRepository.createProductTagWithTx(tx, productId, tag.id); + + return tag.name; + }) + ); + + await productRepository.deleteProductImageWithTx(tx, productId); + + const updatedImages = await Promise.all( + images.map(async (image) => { + const updatedImage = await productRepository.createProductImageWithTx( + tx, + image.filename, + userId, + productId + ); + + return updatedImage.imageUrl; + }) + ); + + return { ...updatedProduct, tags: updatedTags, images: updatedImages }; + }); +}; + +// 상품 삭제 +const deleteProduct = async (productId) => { + return await prisma.$transaction(async (tx) => { + const product = await productRepository.findOnlyProductByIdWithTx( + tx, + productId + ); + + if (!product) { + const error = new Error("이미 삭제된 상품입니다."); + error.code = 404; + + throw error; + } + + return productRepository.deleteProductWithTx(tx, productId); + }); +}; + +// 상품 좋아요 +const addlikeProduct = (userId, productId) => { + return productRepository.addlikeProduct(userId, productId); +}; + +// 상품 좋아요 취소 +const cancelLikeProduct = (userId, productId) => { + return productRepository.cancelLikeProduct(userId, productId); +}; + +export default { + getProducts, + getProduct, + createProduct, + updateProduct, + deleteProduct, + addlikeProduct, + cancelLikeProduct, +}; diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 00000000..e69de29b