diff --git a/package-lock.json b/package-lock.json index 5d825be..b0fca27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,18 +10,23 @@ "license": "ISC", "dependencies": { "ajv": "^8.16.0", + "ajv-formats": "^3.0.1", + "axios": "^1.7.2", "bcrypt": "^5.1.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-session": "^1.18.0", "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", "mongoose": "^8.4.3", "morgan": "^1.10.0", "nodemon": "^3.1.0", "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pm2": "^5.3.1" }, @@ -470,6 +475,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/amp": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", @@ -694,6 +715,11 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -710,6 +736,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -843,6 +879,11 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -993,6 +1034,17 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", @@ -1235,6 +1287,14 @@ "node": ">= 14" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -1288,6 +1348,14 @@ "url": "https://dotenvx.com" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1973,6 +2041,50 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "dependencies": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/express/node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -2162,6 +2274,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3053,6 +3178,46 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -3111,12 +3276,47 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -3903,6 +4103,15 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, "node_modules/passport-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", @@ -4274,6 +4483,14 @@ } ] }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5175,6 +5392,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -5737,6 +5965,14 @@ "uri-js": "^4.4.1" } }, + "ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "requires": { + "ajv": "^8.0.0" + } + }, "amp": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", @@ -5905,6 +6141,11 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5915,6 +6156,16 @@ "possible-typed-array-names": "^1.0.0" } }, + "axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -6020,6 +6271,11 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz", "integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==" }, + "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==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6130,6 +6386,14 @@ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", @@ -6309,6 +6573,11 @@ "esprima": "^4.0.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -6343,6 +6612,14 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" }, + "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==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6879,6 +7156,46 @@ } } }, + "express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "requires": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + }, + "cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "extrareqp2": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/extrareqp2/-/extrareqp2-1.0.0.tgz", @@ -7017,6 +7334,16 @@ "is-callable": "^1.1.3" } }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7659,6 +7986,42 @@ "universalify": "^2.0.0" } }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "requires": { + "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" + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -7702,12 +8065,47 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "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==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "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==" + }, + "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==" + }, + "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==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -8264,6 +8662,15 @@ "utils-merge": "^1.0.1" } }, + "passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "requires": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, "passport-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", @@ -8538,6 +8945,11 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -9174,6 +9586,14 @@ "possible-typed-array-names": "^1.0.0" } }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index ddb1dc0..5fd3dc5 100644 --- a/package.json +++ b/package.json @@ -20,18 +20,23 @@ "homepage": "https://github.com/Murakano/murakano-be#readme", "dependencies": { "ajv": "^8.16.0", + "ajv-formats": "^3.0.1", + "axios": "^1.7.2", "bcrypt": "^5.1.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-session": "^1.18.0", "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", "mongoose": "^8.4.3", "morgan": "^1.10.0", "nodemon": "^3.1.0", "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pm2": "^5.3.1" }, diff --git a/src/common/config/index.js b/src/common/config/index.js index 4b09151..27e15d1 100644 --- a/src/common/config/index.js +++ b/src/common/config/index.js @@ -12,6 +12,12 @@ const conf = { port: process.env.PORT, corsWhiteList: process.env.CORS_WHITELIST, mongoURL: process.env.MONGO_URL, + jwtSecret: process.env.JWT_SECRET, + cookieSecret: process.env.COOKIE_SECRET, + + // social login + kakaoRestApiKey: process.env.KAKAO_REST_API_KEY, + kakaoCallback: process.env.KAKAO_CALLBACK, }; module.exports = conf; diff --git a/src/common/constants/error-message.js b/src/common/constants/error-message.js index c37d89b..ae79ee2 100644 --- a/src/common/constants/error-message.js +++ b/src/common/constants/error-message.js @@ -21,6 +21,8 @@ const ErrorMessage = Object.freeze({ EMAIL_CHECK_ERROR: '이메일 중복검사중 오류가 발생하였습니다.', EXIST_NICKNAME: '이미 존재하는 닉네임 입니다.', EXIST_EMAIL: '이미 존재하는 이메일 입니다.', + LOGIN_ERROR: '로그인중 오류가 발생하였습니다.', + KAKAO_LOGIN_ERROR: '카카오 로그인중 오류가 발생하였습니다.', }); module.exports = ErrorMessage; diff --git a/src/common/constants/success-message.js b/src/common/constants/success-message.js index 9ec1f03..f435bb8 100644 --- a/src/common/constants/success-message.js +++ b/src/common/constants/success-message.js @@ -1,8 +1,13 @@ const SucesssMessage = Object.freeze({ - // USER + // USER - 회원가입 REGISTER_SUCCESSS: '회원가입 성공', AVAILABLE_NICKNAME: '사용 가능한 닉네임입니다.', AVAILABLE_EMAIL: '사용 가능한 이메일입니다.', + + // USER - 로그인 + LOGIN_SUCCESSS: '로그인 성공', + + GET_PROFILE_SUCCESS: '유저 정보 조회 성공', }); module.exports = SucesssMessage; diff --git a/src/common/modules/express/index.js b/src/common/modules/express/index.js index 7343c0f..c9c4da1 100644 --- a/src/common/modules/express/index.js +++ b/src/common/modules/express/index.js @@ -1,14 +1,19 @@ const express = require('express'); const morgan = require('morgan'); +const passport = require('passport'); +const session = require('express-session'); const helmet = require('helmet'); const cors = require('cors'); const crypto = require('crypto'); const cookieParser = require('cookie-parser'); -const conf = require('../../config/index'); +const conf = require('../../config'); +const passportConfig = require('../../passport'); const router = require('../../../routes/index'); module.exports = expressLoader = (app) => { + passportConfig(); + app.use(morgan('dev')); app.use(helmet()); @@ -23,7 +28,7 @@ module.exports = expressLoader = (app) => { cors({ credentials: true, origin: (origin, callback) => { - if (origin !== null || conf.corsWhiteList?.indexOf(origin) !== -1) { + if (origin === undefined || (origin && conf.corsWhiteList?.indexOf(origin) !== -1)) { return callback(null, true); } callback(new Error('CORS ERROR')); @@ -31,6 +36,27 @@ module.exports = expressLoader = (app) => { })(req, res, next); }); + app.use( + session({ + name: 'user', + resave: false, + saveUninitialized: false, + secret: conf.cookieSecret, + cookie: { + // cleint 쿠키 접근 불가 + httpOnly: true, + // TODO : ssl 적용하면 true로 변경 + secure: false, + // 24h + maxAge: 86400000, + }, + }) + ); + + // Passport 세팅 + app.use(passport.initialize()); + app.use(passport.session()); + // Body Parser 세팅 app.use( express.json({ diff --git a/src/common/passport/index.js b/src/common/passport/index.js new file mode 100644 index 0000000..8fc0bdf --- /dev/null +++ b/src/common/passport/index.js @@ -0,0 +1,23 @@ +const passport = require('passport'); +const localStrategy = require('./localStrategy'); +const jwtStrategy = require('./jwtStrategy'); +const User = require('../../routes/user/user.model'); + +module.exports = () => { + passport.serializeUser((user, done) => { + done(null, user.id); + }); + + passport.deserializeUser(async (id, done) => { + try { + const user = await User.findById(id); + done(null, user); + } catch (err) { + done(err); + } + }); + + // 초기화 + localStrategy(); + jwtStrategy(); +}; diff --git a/src/common/passport/jwtStrategy.js b/src/common/passport/jwtStrategy.js new file mode 100644 index 0000000..ae1c388 --- /dev/null +++ b/src/common/passport/jwtStrategy.js @@ -0,0 +1,27 @@ +const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt'); +const passport = require('passport'); +const User = require('../../routes/user/user.model'); +const config = require('../config'); // 비밀 키를 저장한 파일 + +const opts = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: config.jwtSecret, +}; + +module.exports = () => { + passport.use( + new JwtStrategy(opts, async (jwtPayload, done) => { + try { + const user = await User.findById(jwtPayload.id); + if (user) { + return done(null, user); + } else { + return done(null, false); + } + } catch (error) { + console.error(error); + return done(error, false); + } + }) + ); +}; diff --git a/src/common/passport/localStrategy.js b/src/common/passport/localStrategy.js new file mode 100644 index 0000000..5f2bb0b --- /dev/null +++ b/src/common/passport/localStrategy.js @@ -0,0 +1,30 @@ +const passport = require('passport'); +const LocalStrategy = require('passport-local').Strategy; +const User = require('../../routes/user/user.model'); + +module.exports = () => { + passport.use( + new LocalStrategy( + { + usernameField: 'email', + passwordField: 'password', + }, + async (email, password, done) => { + try { + const user = await User.findOne({ email }); + if (!user) { + return done(null, false, { message: '가입되지 않은 회원입니다.' }); + } + const isMatch = await user.comparePassword(password); + if (!isMatch) { + return done(null, false, { message: '비밀번호가 일치하지 않습니다.' }); + } + return done(null, user); + } catch (error) { + console.error(error); + return done(error); + } + } + ) + ); +}; diff --git a/src/common/utils/auth.js b/src/common/utils/auth.js new file mode 100644 index 0000000..d3875e7 --- /dev/null +++ b/src/common/utils/auth.js @@ -0,0 +1,57 @@ +const jwt = require('jsonwebtoken'); +const passport = require('passport'); +const config = require('../config'); + +exports.generateToken = (user) => { + return jwt.sign( + { + id: user._id, + email: user.email, + nickname: user.nickname, + }, + config.jwtSecret, + { expiresIn: '24h' } + ); +}; + +exports.verifyToken = (token) => { + return jwt.verify(token, config.jwtSecret); +}; + +exports.isAuthenticated = (req, res, next) => { + passport.authenticate('jwt', { session: false }, (err, user, info) => { + if (err) { + return res.status(500).json({ message: '서버 오류' }); + } + if (!user) { + return res.status(401).json({ message: '인증되지 않은 사용자' }); + } + req.user = user; + next(); + })(req, res, next); +}; + +exports.isLoggedIn = (req, res, next) => { + passport.authenticate('jwt', { session: false }, (err, user, info) => { + if (err) { + return res.status(500).json({ message: '서버 오류' }); + } + if (!user) { + return res.status(401).json({ message: '로그인이 필요합니다.' }); + } + req.user = user; + next(); + })(req, res, next); +}; + +exports.isNotLoggedIn = (req, res, next) => { + passport.authenticate('jwt', { session: false }, (err, user, info) => { + if (err) { + return res.status(500).json({ message: '서버 오류' }); + } + if (user) { + return res.status(403).json({ message: '이미 로그인된 상태입니다.' }); + } + next(); + })(req, res, next); +}; diff --git a/src/common/utils/kakao.js b/src/common/utils/kakao.js new file mode 100644 index 0000000..00db064 --- /dev/null +++ b/src/common/utils/kakao.js @@ -0,0 +1,37 @@ +const axios = require('axios'); +const conf = require('../../common/config'); + +const header = { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: 'Bearer ', +}; +exports.getKakaoToken = async (code) => { + const data = { + grant_type: 'authorization_code', + client_id: conf.kakaoRestApiKey, + code, + }; + + const queryString = Object.keys(data) + .map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(data[k])) + .join('&'); + + const token = await axios.post('https://kauth.kakao.com/oauth/token', queryString, { headers: header }); + return { accessToken: token.data.access_token }; +}; +exports.getUserInfo = async (accessToken) => { + // Authorization: 'Bearer access_token' + header.Authorization += accessToken; + + // 카카오 사용자 정보 조회 + const get = await axios.get('https://kapi.kakao.com/v2/user/me', { headers: header }); + const result = get.data; + + return { + snsId: result.id, + email: result.kakao_account.email ? result.kakao_account.email : `${result.id}@no.agreement`, + // NOTE: 닉네임 10글자 제한 때문에, 임시 처리 + // kakao 닉네임 규정은 20글자. result.id는 10글자로 추정 + nickname: result.id, + }; +}; diff --git a/src/common/utils/request.validator.js b/src/common/utils/request.validator.js index 7ede557..86e7d54 100644 --- a/src/common/utils/request.validator.js +++ b/src/common/utils/request.validator.js @@ -1,6 +1,8 @@ const ErrorMessage = require('../../common/constants/error-message'); const Ajv = require('ajv'); +const addFormats = require('ajv-formats'); const ajv = new Ajv(); +addFormats(ajv); exports.validateRequest = (schema, req) => { const validate = ajv.compile(schema); diff --git a/src/routes/user/user.controller.js b/src/routes/user/user.controller.js index 13f6aba..c5f7f52 100644 --- a/src/routes/user/user.controller.js +++ b/src/routes/user/user.controller.js @@ -1,21 +1,33 @@ +const passport = require('passport'); const userService = require('./user.service'); const sendResponse = require('../../common/utils/response-handler'); const ErrorMessage = require('../../common/constants/error-message'); const SucesssMessage = require('../../common/constants/success-message'); const { validateRequest } = require('../../common/utils/request.validator'); -const { nicknameCheckReqQuerySchema } = require('./user.schema'); +const { + nicknameCheckReqQuerySchema, + registerBodySchema, + emailCheckReqQuerySchema, + loginBodySchema, +} = require('./user.schema'); +const { generateToken } = require('../../common/utils/auth'); +const { getKakaoToken, getUserInfo } = require('../../common/utils/kakao'); exports.register = async (req, res) => { - // TODO : validation 적용 try { - const newUser = await userService.register(req.body); + const validData = validateRequest(registerBodySchema, req.body); + const newUser = await userService.register(validData); - data = { user_id: newUser._id }; + const data = { user_id: newUser._id }; sendResponse.created(res, { message: SucesssMessage.REGISTER_SUCCESSS, data, }); } catch (err) { + console.log(err); + if (err?.type) { + return sendResponse.badRequest(res, err.message); + } sendResponse.fail(req, res, ErrorMessage.REGISTER_ERROR); } }; @@ -47,10 +59,11 @@ exports.isNicknameExist = async (req, res) => { }; exports.isEmailExist = async (req, res) => { - // TODO : validation 적용 try { - const isUserExist = await userService.isEmailExist(req.query.email); - data = { isUserExist }; + const { email } = validateRequest(emailCheckReqQuerySchema, req.query); + + const isUserExist = await userService.isEmailExist(email); + const data = { isUserExist }; if (isUserExist) { return sendResponse.badRequest(res, { @@ -63,6 +76,68 @@ exports.isEmailExist = async (req, res) => { data, }); } catch (err) { + if (err?.type) { + return sendResponse.badRequest(res, err.message); + } sendResponse.fail(req, res, ErrorMessage.EMAIL_CHECK_ERROR); } }; + +exports.localLogin = async (req, res, next) => { + try { + req.body = validateRequest(loginBodySchema, req.body); + passport.authenticate('local', (authError, user, info) => { + if (authError) { + console.error(authError); + return next(authError); + } + if (!user) { + return sendResponse.unAuthorized(res, { message: info.message }); + } + const token = generateToken(user); + + return sendResponse.ok(res, { + message: SucesssMessage.LOGIN_SUCCESSS, + token, + }); + })(req, res, next); + } catch (err) { + if (err?.type) { + return sendResponse.badRequest(res, err.message); + } + sendResponse.fail(req, res, ErrorMessage.LOGIN_ERROR); + } +}; + +exports.kakaoLogin = async (req, res) => { + try { + const { code } = req.body; + + const { accessToken } = await getKakaoToken(code); + const { snsId, email, nickname } = await getUserInfo(accessToken); + + const kakaoUser = { snsId: snsId, email, nickname, provider: 'kakao' }; + + let user = await userService.isKaKaoUserExist(kakaoUser.snsId); + if (!user) { + user = await userService.kakaoRegister(kakaoUser); + } + const token = generateToken(user); + sendResponse.ok(res, { + message: SucesssMessage.LOGIN_SUCCESSS, + token, + }); + } catch (err) { + console.log(err); + sendResponse.fail(req, res, ErrorMessage.KAKAO_LOGIN_ERROR); + } +}; + +exports.getProfile = (req, res) => { + const { _id, nickname, email } = req.user; + const data = { _id, nickname, email }; + sendResponse.ok(res, { + message: SucesssMessage.GET_PROFILE_SUCCESS, + data, + }); +}; diff --git a/src/routes/user/user.model.js b/src/routes/user/user.model.js index 24076a3..4fae302 100644 --- a/src/routes/user/user.model.js +++ b/src/routes/user/user.model.js @@ -1,14 +1,18 @@ const mongoose = require('mongoose'); +const bcrypt = require('bcrypt'); const userSchema = new mongoose.Schema( { - // TODO : 동작 확인 후 주석 삭제 - // createdAt: { type: Date, default: Date.now }, - // updatedAt: { type: Date, default: Date.now }, deletedAt: { type: Date, default: null }, - nickname: { type: String, required: true, unique: true }, - email: { type: String, required: true, unique: true }, - password: { type: String }, + nickname: { type: String, required: true, unique: true, maxlength: 10, match: /^[a-zA-Z0-9가-힣]+$/ }, + email: { + type: String, + required: true, + unique: true, + maxlength: 50, + match: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/, + }, + password: { type: String, minLength: 8, maxLength: 20 }, role: { type: String, default: 'user' }, snsId: { type: String, default: null }, provider: { type: String, default: null }, @@ -18,6 +22,20 @@ const userSchema = new mongoose.Schema( } ); -const model = mongoose.model('User', userSchema); +userSchema.pre('save', async function (next) { + try { + if ((this.isNew && !this.provider) || this.isModified('password')) { + const salt = await bcrypt.genSalt(10); + this.password = await bcrypt.hash(this.password, salt); + } + } catch (err) { + console.log(err); + } + next(); +}); + +userSchema.methods.comparePassword = async function (password) { + return bcrypt.compare(password, this.password); +}; -module.exports = model; +module.exports = mongoose.model('User', userSchema); diff --git a/src/routes/user/user.repository.js b/src/routes/user/user.repository.js index f2236a5..1b6e995 100644 --- a/src/routes/user/user.repository.js +++ b/src/routes/user/user.repository.js @@ -18,3 +18,8 @@ exports.findUserByEmail = async (email) => { const userExists = await User.exists({ email }); return userExists ? true : false; }; + +exports.getUserBySnsId = async (snsId) => { + const user = await User.findOne({ snsId, provider: 'kakao' }); + return user; +}; diff --git a/src/routes/user/user.route.js b/src/routes/user/user.route.js index 20cd618..e20b27d 100644 --- a/src/routes/user/user.route.js +++ b/src/routes/user/user.route.js @@ -1,9 +1,17 @@ const express = require('express'); -const { register, isNicknameExist, isEmailExist } = require('./user.controller'); +const { register, isNicknameExist, isEmailExist, localLogin, kakaoLogin, getProfile } = require('./user.controller'); +const { isLoggedIn, isNotLoggedIn } = require('../../common/utils/auth'); const userRouter = express.Router(); -userRouter.post('/register', register); +// 회원가입 +userRouter.post('/register', isNotLoggedIn, register); userRouter.get('/check/nickname', isNicknameExist); userRouter.get('/check/email', isEmailExist); +// 로그인 +userRouter.post('/local/login', isNotLoggedIn, localLogin); +userRouter.post('/kakao/login', isNotLoggedIn, kakaoLogin); + +userRouter.get('/profile', isLoggedIn, getProfile); + module.exports = userRouter; diff --git a/src/routes/user/user.schema.js b/src/routes/user/user.schema.js index b4e0d75..699e673 100644 --- a/src/routes/user/user.schema.js +++ b/src/routes/user/user.schema.js @@ -1,14 +1,59 @@ +const commonSchemas = { + nickname: { + type: 'string', + maxLength: 10, + pattern: '^[a-zA-Z0-9가-힣]+$', + }, + email: { + type: 'string', + format: 'email', + maxLength: 50, + }, + password: { + type: 'string', + minLength: 8, + maxLength: 20, + pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-=\\[\\]{};:\'",.<>\\/\\\\?]).+$', + }, +}; + +const registerBodySchema = { + type: 'object', + properties: { + nickname: commonSchemas.nickname, + email: commonSchemas.email, + password: commonSchemas.password, + }, + required: ['nickname', 'email', 'password'], + additionalProperties: false, +}; + const nicknameCheckReqQuerySchema = { type: 'object', properties: { - nickname: { - type: 'string', - maxLength: 10, - pattern: '^[a-zA-Z0-9가-힣]+$', - }, + nickname: commonSchemas.nickname, }, required: ['nickname'], additionalProperties: false, }; -module.exports = { nicknameCheckReqQuerySchema }; +const emailCheckReqQuerySchema = { + type: 'object', + properties: { + email: commonSchemas.email, + }, + required: ['email'], + additionalProperties: false, +}; + +const loginBodySchema = { + type: 'object', + properties: { + email: commonSchemas.email, + password: commonSchemas.password, + }, + required: ['email', 'password'], + additionalProperties: false, +}; + +module.exports = { registerBodySchema, nicknameCheckReqQuerySchema, emailCheckReqQuerySchema, loginBodySchema }; diff --git a/src/routes/user/user.service.js b/src/routes/user/user.service.js index 25d2e25..4c6db44 100644 --- a/src/routes/user/user.service.js +++ b/src/routes/user/user.service.js @@ -1,16 +1,18 @@ -const bcrypt = require('bcrypt'); const userRepository = require('./user.repository'); exports.register = async (userData) => { - const hash = await bcrypt.hash(userData.password, 12); const newUser = { email: userData.email, - password: hash, + password: userData.password, nickname: userData.nickname, }; return await userRepository.createUser(newUser); }; +exports.kakaoRegister = async (newUser) => { + return await userRepository.createUser(newUser); +}; + exports.isNicknameExist = async (nickname) => { const isUserExist = await userRepository.findUserByNickname(nickname); return isUserExist; @@ -20,3 +22,8 @@ exports.isEmailExist = async (email) => { const isUserExist = await userRepository.findUserByEmail(email); return isUserExist; }; + +exports.isKaKaoUserExist = async (snsId) => { + const user = await userRepository.getUserBySnsId(snsId); + return user; +};