diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b082453 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +#Env +.env + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7b9bee4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# syntax = docker/dockerfile:1 + +# Adjust NODE_VERSION as desired +ARG NODE_VERSION=18.18.0 +FROM node:${NODE_VERSION}-slim as base + +LABEL fly_launch_runtime="Node.js" + +# Node.js app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" + + +# Throw-away build stage to reduce size of final image +FROM base as build + +# Install packages needed to build node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 + +# Install node modules +COPY --link package-lock.json package.json ./ +RUN npm ci + +# Copy application code +COPY --link . . + + +# Final stage for app image +FROM base + +# Copy built application +COPY --from=build /app /app + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD [ "node", "src/index.js" ] diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..bc943d3 --- /dev/null +++ b/fly.toml @@ -0,0 +1,22 @@ +# fly.toml app configuration file generated for diversind-back on 2024-03-29T00:04:20-03:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'diversind-back' +primary_region = 'gru' + +[build] + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + memory = '1gb' + cpu_kind = 'shared' + cpus = 1 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dbc8c8e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2325 @@ +{ + "name": "tcc-back", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tcc-back", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bcrypt": "^5.1.1", + "cloudinary": "^1.41.2", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "mongoose": "^8.0.3", + "multer": "^1.4.5-lts.1", + "nodemailer": "^6.9.7", + "randomatic": "^3.1.1", + "socket.io": "^4.7.4" + }, + "devDependencies": { + "@flydotio/dockerfile": "^0.5.4" + } + }, + "node_modules/@flydotio/dockerfile": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@flydotio/dockerfile/-/dockerfile-0.5.4.tgz", + "integrity": "sha512-pglzQeVXVjCk/UCoY+g/jtr4i0zN+1BRTR35rFB4mNWZx/4cxBVa70zH1+N80TvOQ9y3ydB1M8258flr0bM/CQ==", + "dev": true, + "dependencies": { + "chalk": "^5.3.0", + "diff": "^5.1.0", + "ejs": "^3.1.9", + "shell-quote": "^1.8.1", + "yargs": "^17.7.2" + }, + "bin": { + "dockerfile": "index.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.1.tgz", + "integrity": "sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", + "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bson": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.2.0.tgz", + "integrity": "sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==", + "engines": { + "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", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "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", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cloudinary": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-1.41.2.tgz", + "integrity": "sha512-9gH6ofz+N8AzfjqtmTgB9lVxwWcroHFfMOOM6iiX/g1H+KYz+sArClXWtd0prRE6m/IoBBBbbaJ12yRdJNnMxA==", + "dependencies": { + "cloudinary-core": "^2.13.0", + "core-js": "^3.30.1", + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/cloudinary-core": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/cloudinary-core/-/cloudinary-core-2.13.0.tgz", + "integrity": "sha512-Nt0Q5I2FtenmJghtC4YZ3MZZbGg1wLm84SsxcuVwZ83OyJqG9CNIGp86CiI6iDv3QobaqBUpOT7vg+HqY5HxEA==", + "peerDependencies": { + "lodash": ">=4.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "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" + ], + "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==", + "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==" + }, + "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==", + "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==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/core-js": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz", + "integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", + "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.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.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-random": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, + "node_modules/mongodb": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.2.0.tgz", + "integrity": "sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^6.2.0", + "mongodb-connection-string-url": "^2.6.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.0.3.tgz", + "integrity": "sha512-LJRT0yP4TW14HT4r2RkxqyvoTylMSzWpl5QOeVHTnRggCLQSpkoBdgbUtORFq/mSL2o9cLCPJz+6uzFj25qbHw==", + "dependencies": { + "bson": "^6.2.0", + "kareem": "2.5.1", + "mongodb": "6.2.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "16.0.1" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "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==", + "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", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/nodemailer": { + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.12.tgz", + "integrity": "sha512-pnLo7g37Br3jXbF0bl5DekBJihm2q+3bB3l2o/B060sWmb5l+VqeScAQCBqaQ+5ezRZFzW5SciZNGdRDEbq89w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "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==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dependencies": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "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", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" + }, + "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==" + }, + "node_modules/socket.io": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", + "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "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==", + "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==", + "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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "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==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "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==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..739fc20 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "tcc-back", + "version": "1.0.0", + "description": "", + "main": "src/index.js", + "type": "module", + "scripts": { + "dev": "node --watch-path=./src src/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "bcrypt": "^5.1.1", + "cloudinary": "^1.41.2", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "mongoose": "^8.0.3", + "multer": "^1.4.5-lts.1", + "nodemailer": "^6.9.7", + "randomatic": "^3.1.1", + "socket.io": "^4.7.4" + }, + "devDependencies": { + "@flydotio/dockerfile": "^0.5.4" + } +} diff --git a/src/config/allowedOrigins.js b/src/config/allowedOrigins.js new file mode 100644 index 0000000..8a170b4 --- /dev/null +++ b/src/config/allowedOrigins.js @@ -0,0 +1,8 @@ +import dotenv from "dotenv" + +dotenv.config() + +export const allowedOrigins = [ + process.env.FRONTEND_LOCAL, + process.env.FRONTEND_PROD, +] diff --git a/src/config/corsOptions.js b/src/config/corsOptions.js new file mode 100644 index 0000000..0b6c9a7 --- /dev/null +++ b/src/config/corsOptions.js @@ -0,0 +1,12 @@ +import { allowedOrigins } from "./allowedOrigins.js" + +export const corsOptions = { + origin: (origin, callback) => { + if (allowedOrigins.indexOf(origin) !== -1 || !origin) { + callback(null, true) + } else { + callback(new Error("Not allowed by CORS")) + } + }, + optionsSuccessStatus: 200, +} diff --git a/src/controllers/comment.controller.js b/src/controllers/comment.controller.js new file mode 100644 index 0000000..31d62b7 --- /dev/null +++ b/src/controllers/comment.controller.js @@ -0,0 +1,58 @@ +import * as Post from "../models/post.model.js" +import * as Comment from "../models/comment.model.js" + +export async function newComment(req, res) { + try { + const { postId, content } = req.body + const { userId } = req + + const comment = await Comment.createComment({ + post: postId, + user: userId, + content, + }) + await Post.addCommentPost(postId, comment._id) + + res.status(201).json({ comment, message: "Comentário enviado" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: error.message }) + } +} + +export async function getCommentsFromPost(req, res) { + try { + const { postId, page = 1 } = req.body + const { comments, totalPages } = await Comment.getComments(postId, page) + + res.status(200).json({ comments, totalPages }) + } catch (error) { + res.status(500).json({ message: error.message }) + } +} + +export async function editComment(req, res) { + try { + const { commentId, content } = req.body + + await Comment.updateComment(commentId, content) + res.status(200).json({ message: "Comentário editado" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: error.message }) + } +} + +export async function removeComment(req, res) { + try { + const { commentId, postId } = req.body + + await Comment.deleteComment(commentId) + await Post.deletePostComment(postId, commentId) + + res.status(200).json({ message: "Comentário removido" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: error.message }) + } +} diff --git a/src/controllers/community.controller.js b/src/controllers/community.controller.js new file mode 100644 index 0000000..d59ec33 --- /dev/null +++ b/src/controllers/community.controller.js @@ -0,0 +1,161 @@ +import * as CommunityModel from "../models/community.model.js" + +export async function createCommunity(req, res) { + try { + const { userId } = req + const { name, description, link, platform, professionalArea, skills } = + req.body + + let formattedLink = link.toLowerCase().replace(/\//g, "") + formattedLink = formattedLink.replace("https:", "") + formattedLink = formattedLink.replace("www.", "") + const communityExists = await CommunityModel.getCommunityByFormattedLink( + formattedLink, + ) + + if (communityExists) { + return res.status(400).send({ message: "Comunidade já cadastrada" }) + } + + const community = { + name, + author: userId, + description, + link, + formattedLink, + platform, + professionalArea, + skills, + } + + await CommunityModel.create(community) + res.status(201).json({ message: "Comunidade criada" }) + } catch (error) { + console.log("Controller - Erro ao criar comunidade: ", error) + res.status(500).send({ message: "Erro ao criar comunidade" }) + } +} + +export async function editCommunity(req, res) { + try { + const { userId } = req + const { + communityId, + name, + description, + link, + platform, + professionalArea, + skills, + } = req.body + + const foundedCommunity = await CommunityModel.getCommunityById(communityId) + + if (!foundedCommunity) { + return res.status(404).send({ message: "Comunidade não encontrada" }) + } + + if (foundedCommunity.author.toString() !== userId) { + return res.status(403).send({ message: "Usuário não autorizado" }) + } + + let formattedLink = link.toLowerCase().replace(/\//g, "") + formattedLink = formattedLink.replace("https:", "") + formattedLink = formattedLink.replace("www.", "") + const communityExists = await CommunityModel.getCommunityByFormattedLink( + formattedLink, + ) + + if (communityExists && communityExists._id.toString() !== communityId) { + return res.status(400).send({ message: "Comunidade já cadastrada" }) + } + + const community = { + name, + description, + link, + formattedLink, + platform, + professionalArea, + skills, + } + + await CommunityModel.update(communityId, community) + res.status(200).json({ message: "Comunidade editada" }) + } catch (error) { + console.log("Controller - Erro ao editar comunidade: ", error) + res.status(500).send({ message: "Erro ao editar comunidade" }) + } +} + +export async function listCommunities(req, res) { + try { + const { page = 1, limit = 5, filters, keyword = "" } = req.body + const { communities, totalCommunities } = await CommunityModel.list({ + page, + limit, + filters, + keyword, + }) + + res.status(200).json({ communities, totalCommunities }) + } catch (error) { + console.log("Controller - Erro ao listar todas as vagas: ", error) + res.status(500).send({ message: "Erro ao listar todas as vagas" }) + } +} + +export async function listCommunitiesByAuthor(req, res) { + try { + const { userId } = req + const { page = 1, limit = 5, filters, keyword = "" } = req.body + const { communities, totalCommunities } = await CommunityModel.list({ + userId, + page, + limit, + filters, + keyword, + }) + + res.status(200).json({ communities, totalCommunities }) + } catch (error) { + console.log("Controller - Erro ao listar todas as vagas: ", error) + res.status(500).send({ message: "Erro ao listar todas as vagas" }) + } +} + +export async function rateCommunity(req, res) { + try { + const { userId } = req + const { rating, communityId } = req.body + await CommunityModel.updateRating({ userId, rating, communityId }) + + res.status(200).json({ message: "Comunidade avaliada" }) + } catch (error) { + console.log("Controller - Erro ao avaliar comunidade: ", error) + res.status(500).send({ message: "Erro ao avaliar comunidade" }) + } +} + +export async function deleteCommunity(req, res) { + try { + const { userId } = req + const { communityId } = req.params + + const foundedCommunity = await CommunityModel.getCommunityById(communityId) + if (!foundedCommunity) { + return res.status(404).send({ message: "Comunidade não encontrada" }) + } + + if (foundedCommunity.author.toString() !== userId) { + return res.status(403).send({ message: "Usuário não autorizado" }) + } + + await CommunityModel.remove(communityId) + + res.status(200).json({ message: "Comunidade removida" }) + } catch (error) { + console.log("Controller - Erro ao remover comunidade: ", error) + res.status(500).send({ message: "Erro ao remover comunidade" }) + } +} diff --git a/src/controllers/conversation.controller.js b/src/controllers/conversation.controller.js new file mode 100644 index 0000000..a4bad6d --- /dev/null +++ b/src/controllers/conversation.controller.js @@ -0,0 +1,45 @@ +import * as Conversation from "../models/conversation.model.js" + +export async function listConversations(req, res) { + try { + const { userId } = req + const { keyword } = req.params + + const conversations = await Conversation.listByUser(userId, keyword) + const formattedConversations = conversations.map((conversation) => { + const receiver = conversation.participants.find( + (participant) => participant._id != userId, + ) + return { + conversationId: conversation._id, + receiver: receiver, + lastMessage: conversation.messages[0], + } + }) + + res.status(200).json({ conversations: formattedConversations }) + } catch (error) { + console.log("Controller - Erro ao buscar conversa:", error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function getConversationMessages(req, res) { + try { + const { conversationId, page = 1, limit = 10 } = req.params + + const conversation = await Conversation.getConversationById( + conversationId, + page, + limit, + ) + if (!conversation) { + return res.status(404).json({ message: "Conversa não encontrada" }) + } + + res.status(200).json(conversation) + } catch (error) { + console.log("Controller - Erro ao buscar mensagens da conversa:", error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} diff --git a/src/controllers/like.controller.js b/src/controllers/like.controller.js new file mode 100644 index 0000000..29af5ae --- /dev/null +++ b/src/controllers/like.controller.js @@ -0,0 +1,31 @@ +import * as Like from "../models/like.model.js" +import * as Post from "../models/post.model.js" + +export async function likePost(req, res) { + try { + const { postId } = req.body + const { userId } = req + + const like = await Like.newLike(postId, userId) + await Post.addLikePost(postId, userId) + + res.status(201).json({ like, message: "Post curtido" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: error.message }) + } +} + +export async function unlikePost(req, res) { + try { + const { postId } = req.body + const { userId } = req + + await Like.removeLike(postId, userId) + await Post.removeLikePost(postId, userId) + res.status(200).json({ message: "Post descurtido" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: error.message }) + } +} diff --git a/src/controllers/message.controller.js b/src/controllers/message.controller.js new file mode 100644 index 0000000..7ea19b0 --- /dev/null +++ b/src/controllers/message.controller.js @@ -0,0 +1,49 @@ +import * as Message from "../models/message.model.js" +import * as Conversation from "../models/conversation.model.js" +import { getReceiverSocketId, io } from "../socket/socket.js" + +export async function sendMessage(req, res) { + try { + const { userId } = req + const { content, receiverId } = req.body + + const messageBody = { sender: userId, receiver: receiverId, content } + const newMessage = await Message.create(messageBody) + + let conversationId = null + const foundedConversation = + await Conversation.getConversationByParticipants({ + userId, + receiverId, + }) + + if (foundedConversation) { + conversationId = foundedConversation._id + await Conversation.addMessage(foundedConversation._id, newMessage._id) + } else { + const conversationBody = { + participants: [userId, receiverId], + messages: [newMessage._id], + } + const newConversation = await Conversation.create(conversationBody) + conversationId = newConversation._id + } + + const receiverSocketId = getReceiverSocketId(receiverId) + console.log("receiverSocketId", receiverSocketId) + if (receiverSocketId) { + io.to(receiverSocketId).emit("newMessage", { + ...newMessage._doc, + conversationId, + }) + } + + res.status(201).json({ + message: "Mensagem enviada", + newMessage: { ...newMessage._doc, conversationId }, + }) + } catch (error) { + console.log("Controller - Erro ao enviar mensagem:", error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} diff --git a/src/controllers/posts.controller.js b/src/controllers/posts.controller.js new file mode 100644 index 0000000..60c4f25 --- /dev/null +++ b/src/controllers/posts.controller.js @@ -0,0 +1,139 @@ +import { unlink } from "fs/promises" + +// Models +import * as Post from "../models/post.model.js" +import * as User from "../models/user.model.js" +import * as Comment from "../models/comment.model.js" +import * as Like from "../models/like.model.js" + +// Midlewares +import cloudinary from "../middlewares/cloudinary.js" + +export async function createPost(req, res) { + try { + const { content, mediaDescription } = req.body + const file = req.file + const { userId } = req + + let postBody = { content, author: userId } + if (file) { + const result = await cloudinary.uploader.upload(file.path) + postBody["media"] = result.secure_url + postBody["cloudinaryId"] = result.public_id + postBody["mediaDescription"] = mediaDescription + } + + const post = await Post.newPost(postBody) + const userFollowers = await User.getFollowers(userId) + + await User.addInFeed(userFollowers, post._id) + await User.addInFeed(userId, post._id) + + if (file) { + await unlink(file.path) + } + + res.status(201).json({ post, message: "Postagem criada com sucesso" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro ao criar publicação" }) + } +} + +export async function editPost(req, res) { + try { + const { content, mediaDescription, postId, mediaRemoved = false } = req.body + const file = req.file + + const foundedPost = await Post.getPostById(postId) + + let postBody = { content } + + if (foundedPost.cloudinaryId && (file || mediaRemoved)) { + await cloudinary.uploader.destroy(foundedPost.cloudinaryId) + } + + if (file) { + const result = await cloudinary.uploader.upload(file.path) + postBody["media"] = result.secure_url + postBody["cloudinaryId"] = result.public_id + postBody["mediaDescription"] = mediaDescription + } + + if (mediaRemoved && foundedPost.cloudinaryId) { + postBody["media"] = null + postBody["cloudinaryId"] = null + postBody["mediaDescription"] = null + } + + const post = await Post.updatePost(postId, postBody) + + if (file) { + await unlink(file.path) + } + + res.status(200).json({ post, message: "Alterações salvas" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: error.message }) + } +} + +export async function getFeed(req, res) { + try { + const { userId } = req + const { page = 1 } = req.query + const { posts, total } = await User.getFeedPosts(userId, page) + + if (posts.length === 0) { + const postsRandom = await Post.feedRandom() + return res + .status(200) + .json({ posts: postsRandom, total: 1, isRandom: true }) + } + res.status(200).json({ posts, total, isRandom: false }) + } catch (error) { + console.log(error) + res.status(500).json({ message: error.message }) + } +} + +export async function getPost(req, res) { + try { + const { postId } = req.query + const post = await Post.getPostById(postId) + + res.status(200).json({ post }) + } catch (error) { + res.status(500).json({ message: error.message }) + } +} + +export async function deletePost(req, res) { + try { + const { postId } = req.query + const { userId } = req + + const post = await Post.getPostById(postId) + + if (post.author._id.toString() !== userId) { + return res + .status(403) + .json({ message: "Você não tem permissão para deletar essa postagem" }) + } + + if (post.cloudinaryId) { + await cloudinary.uploader.destroy(post.cloudinaryId) + } + + await Post.removePost(postId) + await User.removeFromFeed(userId, postId) + await Comment.removeManyComments(postId) + await Like.removeManyLikes(postId) + + res.status(200).json({ message: "Postagem deletada com sucesso" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: error.message }) + } +} diff --git a/src/controllers/professionalArea.controller.js b/src/controllers/professionalArea.controller.js new file mode 100644 index 0000000..26e0424 --- /dev/null +++ b/src/controllers/professionalArea.controller.js @@ -0,0 +1,24 @@ +// Models +import * as ProfessionalAreaSchema from "../models/professionalArea.model.js" + +export async function addProfessionalArea(req, res) { + try { + const { name } = req.body + await ProfessionalAreaSchema.createProfessionalArea(name) + res.status(201).json({ message: "Área profissional criada com sucesso" }) + } catch (error) { + console.log("DB - Erro ao criar área profissional: ", error) + throw new Error(error) + } +} + +export async function listProfessionalAreas(req, res) { + try { + const professionalAreas = await ProfessionalAreaSchema.getAll() + + res.status(200).json(professionalAreas) + } catch (error) { + console.log("DB - Erro ao listar áreas profissionais: ", error) + throw new Error(error) + } +} diff --git a/src/controllers/skill.controller.js b/src/controllers/skill.controller.js new file mode 100644 index 0000000..cf79d04 --- /dev/null +++ b/src/controllers/skill.controller.js @@ -0,0 +1,36 @@ +// Models +import * as SkillSchema from "../models/skill.model.js" + +export async function addSkill(req, res) { + try { + const { skillName, professionalAreaId } = req.body + await SkillSchema.createSkill(skillName, professionalAreaId) + } catch (error) { + console.log("DB - Erro ao criar competência: ", error) + throw new Error(error) + } +} + +export async function addManySkills(req, res) { + try { + const { skills, professionalAreaId } = req.body + await SkillSchema.createManySkill(skills, professionalAreaId) + + res.status(201).json({ message: "Competências criadas com sucesso" }) + } catch (error) { + console.log("DB - Erro ao criar competência: ", error) + throw new Error(error) + } +} + +export async function getSkillByProfessinalArea(req, res) { + try { + const { professionalAreaId } = req.params + const skills = await SkillSchema.getByProfessionalArea(professionalAreaId) + + res.status(200).json(skills) + } catch (error) { + console.log("DB - Erro ao listar competências: ", error) + throw new Error(error) + } +} diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js new file mode 100644 index 0000000..ed28759 --- /dev/null +++ b/src/controllers/user.controller.js @@ -0,0 +1,1142 @@ +import bcrypt from "bcrypt" +import jwt from "jsonwebtoken" +import dotenv from "dotenv" +import { unlink } from "fs/promises" +import { ObjectId } from "mongodb" +import randomize from "randomatic" + +// Models +import * as User from "../models/user.model.js" +import * as Post from "../models/post.model.js" +import * as Comment from "../models/comment.model.js" +import * as Application from "../models/applicationVacancy.model.js" +import * as Like from "../models/like.model.js" +import * as Conversation from "../models/conversation.model.js" +import * as Message from "../models/message.model.js" + +// Middlewares +import cloudinary from "../middlewares/cloudinary.js" + +// Utils +import { sendVerificationEmail } from "../utils/sendVerificationEmail.js" +import { sendForgotPasswordEmail } from "../utils/sendForgotPasswordEmail.js" +import { + sortCertificates, + sortExperiences, + sortFormation, +} from "../utils/index.js" +import { sendChangeEmailCode } from "../utils/sendChangeEmailCode.js" + +dotenv.config() + +export async function getAllUsers(req, res) { + try { + const users = await User.listAll() + + res.json(users) + } catch (error) { + res.status(500).json({ message: error.message }) + } +} + +export async function getUser(req, res) { + try { + const { id } = req.query + + const user = await User.getById(id) + + if (!user) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + res.status(200).json(user) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function signUpPerson(req, res) { + try { + const { name, email, password } = req.body + + const foundUser = await User.findByEmail(email) + + if (foundUser) { + return res.status(409).json({ message: "Usuário já cadastrado" }) + } + + const hashedPassword = await bcrypt.hash(password, 10) + const emailToken = jwt.sign({ email }, process.env.EMAIL_TOKEN_SECRET, { + expiresIn: "5m", + }) + + await User.create({ + name, + email, + password: hashedPassword, + profileType: "person", + emailToken, + }) + await sendVerificationEmail({ name, email, emailToken }) + + res + .status(201) + .json({ message: "Enviamos um link de confirmação para o seu e-mail" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function signUpCompany(req, res) { + try { + const { + name, + email, + password, + city, + stateUf, + occupationArea, + companyType, + website, + } = req.body + + const foundUser = await User.findByEmail(email) + + if (foundUser) { + return res.status(409).json({ message: "Usuário já cadastrado" }) + } + + const hashedPassword = await bcrypt.hash(password, 10) + const emailToken = jwt.sign({ email }, process.env.EMAIL_TOKEN_SECRET, { + expiresIn: "5m", + }) + + await User.create({ + name, + email, + password: hashedPassword, + city, + stateUf, + occupationArea, + companyType, + website, + profileType: "company", + emailToken, + }) + await sendVerificationEmail({ name, email, emailToken }) + + res + .status(201) + .json({ message: "Enviamos um link de confirmação para o seu e-mail" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function signIn(req, res) { + try { + const { email, password } = req.body + + const foundUser = await User.findByEmail(email) + + if (!foundUser) { + return res.status(401).json({ message: "E-mail ou senha incorretos" }) + } + + const isPasswordMatch = await bcrypt.compare(password, foundUser.password) + + if (!isPasswordMatch) { + return res.status(401).json({ message: "E-mail ou senha incorretos" }) + } + + if (foundUser && !foundUser.active) { + let expiresTime = 0 + if (foundUser.emailToken) { + expiresTime = jwt.decode( + foundUser.emailToken, + process.env.EMAIL_TOKEN_SECRET, + ).exp + } + return res.status(403).json({ message: "Usuário inativo", expiresTime }) + } + + /* Create access token */ + const accessToken = jwt.sign( + { email: foundUser.email, _id: foundUser._id }, + process.env.ACCESS_TOKEN_SECRET, + { expiresIn: "15m" }, + ) + + const refreshToken = jwt.sign( + { userId: foundUser._id }, + process.env.REFRESH_TOKEN_SECRET, + { expiresIn: "1d" }, + ) + + await User.update(foundUser.email, { refreshToken }) + + res.cookie("rf-jwt", refreshToken, { + httpOnly: true, + secure: true, + sameSite: "none", + maxAge: 24 * 60 * 60 * 1000, // 1 day in ms + }) + const { + name, + email: userEmail, + avatar, + headline, + followers, + following, + resumeUrl, + _id, + } = foundUser + + res.json({ + name, + email: userEmail, + headline, + resumeUrl, + followers: followers.length, + following: following.length, + _id, + avatar, + accessToken, + }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function signOut(req, res) { + try { + const cookies = req.cookies + + if (!cookies ?? !cookies["rf-jwt"]) { + return res.sendStatus(204) + } + + const refreshToken = cookies["rf-jwt"] + + const foundUser = await User.findByRefreshToken(refreshToken) + + if (!foundUser) { + res.clearCookie("rf-jwt", { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }) + return res.sendStatus(204) + } + + await User.update(foundUser.email, { refreshToken: "" }) + + res.clearCookie("rf-jwt", { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }) + res.sendStatus(204) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function refreshAccessToken(req, res) { + const cookies = req.cookies + if (!cookies || !cookies["rf-jwt"]) { + return res.status(401).json({ message: "Não autorizado" }) + } + + const refreshToken = cookies["rf-jwt"] + + const foundUser = await User.findByRefreshToken(refreshToken) + + if (!foundUser) { + return res.status(401).json({ message: "Acesso não autorizado" }) + } + + jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, payload) => { + if (err || payload.userId !== foundUser._id.toString()) { + return res.status(401).json({ message: "Acesso não autorizado" }) + } + + const accessToken = jwt.sign( + { email: foundUser.email, _id: foundUser._id }, + process.env.ACCESS_TOKEN_SECRET, + { expiresIn: "15m" }, + ) + + res.json({ accessToken }) + }) +} + +export async function resendConfirmationEmail(req, res) { + try { + const { email } = req.body + + const foundUser = await User.findByEmail(email) + + if (foundUser) { + const emailToken = jwt.sign({ email }, process.env.EMAIL_TOKEN_SECRET, { + expiresIn: "30000ms", + }) + + await User.update(foundUser.email, { emailToken }) + await sendVerificationEmail({ name: foundUser.name, email, emailToken }) + } + + res.status(200).json({ + message: "Enviamos um novo link de confirmação para o seu e-mail", + }) + } catch (error) { + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function confirmEmail(req, res) { + try { + const { token } = req.body + + const { email } = jwt.verify(token, process.env.EMAIL_TOKEN_SECRET) + + const foundUser = await User.findByEmail(email) + + if (!foundUser) { + return res + .status(400) + .json({ message: "Não foi possível confirmar o e-mail" }) + } + + const { emailToken } = foundUser + + if (token !== emailToken) { + return res + .status(400) + .json({ message: "Não foi possível confirmar o e-mail" }) + } + + await User.update(foundUser.email, { active: true, emailToken: "" }) + + res.status(200).json({ message: "E-mail confirmado com sucesso" }) + } catch (error) { + if (error.message !== "jwt expired") console.log(error) + + if (error.message === "jwt expired") { + return res.status(400).json({ + message: + "Código de confirmação expirado. Envie um novo e-mail de confirmação", + }) + } + + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function forgotPassword(req, res) { + try { + const { email } = req.body + + const foundUser = await User.findByEmail(email) + + if (!foundUser) { + return res.status(200).json({ + message: + "Se o e-mail estiver vinculado à sua conta, você receberá instruções para redefinir a senha.", + }) + } + + if (foundUser.forgotPasswordToken) { + const { iat } = jwt.decode( + foundUser.forgotPasswordToken, + process.env.FORGOT_PASSWORD_SECRET, + ) + const createdAt = iat * 1000 + const now = new Date().getTime() + + const diffInMinutes = Math.abs(now - createdAt) / (1000 * 60) // 1000ms * 60s = 1min + + if (diffInMinutes < 2) { + const timeToResend = createdAt + 120000 + return res.status(400).json({ + message: "Aguarde para enviar um novo e-mail de redefinição de senha", + timeToResend, + }) + } + } + + const forgotPasswordToken = jwt.sign( + { email }, + process.env.FORGOT_PASSWORD_SECRET, + { + expiresIn: "5m", + }, + ) + + await User.update(foundUser.email, { forgotPasswordToken }) + await sendForgotPasswordEmail({ + name: foundUser.name, + email, + forgotPasswordToken, + }) + + res.status(200).json({ + message: + "Se o e-mail estiver vinculado à sua conta, você receberá instruções para redefinir a senha.", + }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function resetPassword(req, res) { + try { + const { token, password } = req.body + + const { email } = jwt.verify(token, process.env.FORGOT_PASSWORD_SECRET) + + const foundUser = await User.findByEmail(email) + + if (!foundUser) { + return res + .status(400) + .json({ message: "Não foi possível redefinir a senha" }) + } + + if (token !== foundUser.forgotPasswordToken) { + return res + .status(400) + .json({ message: "Não foi possível redefinir a senha" }) + } + + const hashedPassword = await bcrypt.hash(password, 10) + + await User.update(foundUser.email, { + password: hashedPassword, + forgotPasswordToken: "", + }) + + res.status(200).json({ + message: + "Senha redefinida com sucesso. Você será redirecionado para a página de login", + }) + } catch (error) { + if (error.message !== "jwt expired") console.log(error) + + if (error.message === "jwt expired") { + return res.status(400).json({ + message: "Código expirado. Envie um novo e-mail para alterar a senha", + }) + } + + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function addFollower(req, res) { + try { + const { userId } = req + const { followerId } = req.body + + await User.addNewFollowing(userId, followerId) + await User.addNewFollower(followerId, userId) + res.status(200).json({ message: "Seguindo" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function removeFollower(req, res) { + try { + const { userId } = req + const { followerId } = req.body + + await User.removeFollower(userId, followerId) + res.status(200).json({ message: "Seguidor removido" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function updateBasicInfo(req, res) { + try { + const { userId } = req + const { name, headline, stateUf, city } = req.body + + const updatedUser = await User.updateById(userId, { + name, + headline, + stateUf, + city, + }) + const { email: userEmail, avatar, followers, following, _id } = updatedUser + + res.status(200).json({ + message: "Informações atualizadas", + user: { + name, + email: userEmail, + avatar, + headline, + followers: followers.length, + following: following.length, + _id, + }, + }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function updateAvatar(req, res) { + try { + const { file, userId } = req + + const foundedUser = await User.getById(userId) + + if (!foundedUser) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + if (foundedUser.avatarCloudinaryId) { + await cloudinary.uploader.destroy(foundedUser.avatarCloudinaryId) + } + + let avatarInfo = {} + const result = await cloudinary.uploader.upload(file.path) + avatarInfo["avatar"] = result.secure_url + avatarInfo["avatarCloudinaryId"] = result.public_id + + const updatedUser = await User.updateById(userId, avatarInfo) + const { + name, + email: userEmail, + avatar, + headline, + followers, + following, + _id, + } = updatedUser + await unlink(file.path) + + res.status(200).json({ + message: "Foto atualizada", + user: { + name, + email: userEmail, + avatar, + headline, + followers: followers.length, + following: following.length, + _id, + }, + }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export function updateAbout(req, res) { + try { + const { userId } = req + const { about } = req.body + + const foundedUser = User.getById(userId) + + if (!foundedUser) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + User.updateById(userId, { about }) + + res.status(200).json({ message: "Descrição atualizada" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function addExperience(req, res) { + try { + const { userId } = req + const { + occupation, + company, + startDateMonth, + startDateYear, + endDateMonth, + endDateYear, + current = false, + type, + description, + } = req.body + let endMonth = endDateMonth + let endYear = endDateYear + + if (current) { + endMonth = null + endYear = null + } + + const foundedUser = await User.getById(userId) + + if (!foundedUser) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + let prevExperiences = foundedUser.experience + let newExperience = { + _id: new ObjectId(), + occupation, + company, + startDateMonth, + startDateYear, + endDateMonth: endMonth, + endDateYear: endYear, + current, + type, + description, + createdAt: new Date(), + } + prevExperiences.push(newExperience) + + let newExperiences = sortExperiences(prevExperiences) + + await User.updateExperience(userId, newExperiences) + + res.status(200).json({ message: "Experiência adicionada" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function editExperience(req, res) { + try { + const { userId } = req + const { + experienceId, + occupation, + company, + startDateMonth, + startDateYear, + endDateMonth, + endDateYear, + current = false, + type, + description, + } = req.body + let endMonth = endDateMonth + let endYear = endDateYear + + if (current) { + endMonth = null + endYear = null + } + + const foundedUser = await User.getById(userId) + + if (!foundedUser) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + let prevExperiences = foundedUser.experience + let newExperiences = prevExperiences.map((experience) => { + if (experience._id.toString() === experienceId) { + return { + _id: experience._id, + occupation, + company, + startDateMonth, + startDateYear, + endDateMonth: endMonth, + endDateYear: endYear, + current, + type, + description, + createdAt: experience.createdAt, + } + } + return experience + }) + + newExperiences = sortExperiences(newExperiences) + + await User.updateExperience(userId, newExperiences) + + res.status(200).json({ message: "Experiência atualizada" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function deleteExperience(req, res) { + try { + const { userId } = req + const { experienceId } = req.query + + const foundedUser = await User.getById(userId) + + if (!foundedUser) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + let prevExperiences = foundedUser.experience + let newExperiences = prevExperiences.filter( + (experience) => experience._id.toString() !== experienceId, + ) + + await User.updateExperience(userId, newExperiences) + + res.status(200).json({ message: "Experiência removida" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function addAcademicEducation(req, res) { + try { + const { userId } = req + const { + name, + institution, + degree, + startDateMonth, + startDateYear, + endDateMonth, + endDateYear, + } = req.body + + const foundedUser = await User.getById(userId) + + let prevFormations = foundedUser.academicEducation + let newFormation = { + _id: new ObjectId(), + name, + institution, + degree, + startDateMonth, + startDateYear, + endDateMonth, + endDateYear, + createdAt: new Date(), + } + prevFormations.push(newFormation) + + let newFormations = sortFormation(prevFormations) + await User.updateAcademicEducation(userId, newFormations) + + res.status(200).json({ message: "Formação adicionada" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function editAcademicEducation(req, res) { + try { + const { userId } = req + const { + academicEducationId, + name, + institution, + degree, + startDateMonth, + startDateYear, + endDateMonth, + endDateYear, + } = req.body + + const foundedUser = await User.getById(userId) + + if (!foundedUser) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + let prevFormations = foundedUser.academicEducation + let newFormations = prevFormations.map((formation) => { + if (formation._id.toString() === academicEducationId) { + return { + _id: formation._id, + name, + institution, + degree, + startDateMonth, + startDateYear, + endDateMonth, + endDateYear, + createdAt: formation.createdAt, + } + } + return formation + }) + + newFormations = sortFormation(newFormations) + + await User.updateAcademicEducation(userId, newFormations) + + res.status(200).json({ message: "Formação atualizada" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function deleteAcademicEducation(req, res) { + try { + const { userId } = req + const { academicEducationId } = req.query + + const foundedUser = await User.getById(userId) + + if (!foundedUser) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + let prevFormations = foundedUser.academicEducation + let newFormations = prevFormations.filter( + (formation) => formation._id.toString() !== academicEducationId, + ) + + await User.updateAcademicEducation(userId, newFormations) + + res.status(200).json({ message: "Experiência removida" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function addCertificate(req, res) { + try { + const { userId } = req + const { name, institution, issueMonth, issueYear, url } = req.body + + const foundedUser = await User.getById(userId) + + let prevCertificates = foundedUser.certificates + let newCertificate = { + _id: new ObjectId(), + name, + institution, + issueMonth, + issueYear, + url, + createdAt: new Date(), + } + prevCertificates.push(newCertificate) + + let newCertificates = sortCertificates(prevCertificates) + + await User.updateCertificates(userId, newCertificates) + + res.status(200).json({ message: "Certificado adicionado" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function editCertificate(req, res) { + try { + const { userId } = req + const { certificateId, name, institution, issueMonth, issueYear, url } = + req.body + + const foundedUser = await User.getById(userId) + + if (!foundedUser) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + let prevCertificates = foundedUser.certificates + let newCertificates = prevCertificates.map((certificate) => { + if (certificate._id.toString() === certificateId) { + return { + _id: certificate._id, + name, + institution, + issueMonth, + issueYear, + url, + createdAt: certificate.createdAt, + } + } + return certificate + }) + + newCertificates = sortCertificates(newCertificates) + + await User.updateCertificates(userId, newCertificates) + + res.status(200).json({ message: "Certificado atualizado" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function deleteCertificate(req, res) { + try { + const { userId } = req + const { certificateId } = req.query + + const foundedUser = await User.getById(userId) + + if (!foundedUser) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + let prevCertificates = foundedUser.certificates + let newCertificates = prevCertificates.filter( + (certificate) => certificate._id.toString() !== certificateId, + ) + + await User.updateCertificates(userId, newCertificates) + + res.status(200).json({ message: "Certificado removido" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function updateResume(req, res) { + try { + const { file, userId } = req + + if (!file) { + return res.status(400).json({ message: "Nenhum arquivo enviado" }) + } + + const foundedUser = await User.getById(userId) + if (foundedUser.resumeCloudinaryId) { + await cloudinary.uploader.destroy(foundedUser.resumeCloudinaryId) + } + + const result = await cloudinary.uploader.upload(file.path, { + resource_type: "raw", + filename_override: file.originalname, + }) + + await User.updateById(userId, { resumeUrl: result.url }) + await unlink(file.path) + res + .status(200) + .json({ message: "Currículo atualizado", resumeUrl: result.url }) + } catch (error) { + console.log(error) + } +} + +export async function getNetworkUsers(req, res) { + try { + const { userId } = req + const { page, limit, type, keyword } = req.body + + if (type === "followers") { + const { users, total } = await User.listAllFollowers({ + userId, + page, + limit, + keyword, + }) + return res.status(200).json({ users, total }) + } + + if (type === "following") { + const { users, total } = await User.listAllFollowing({ + userId, + page, + limit, + keyword, + }) + return res.status(200).json({ users, total }) + } + + if (type === "all") { + const { users, total } = await User.listAllUsers({ + userId, + page, + limit, + keyword, + }) + + return res.status(200).json({ users, total }) + } + + const { users, total } = await User.listUserNotFollowing({ + userId, + page, + limit, + keyword, + }) + + res.status(200).json({ users, total }) + } catch (error) { + console.log(error) + } +} + +export async function getNetworkingUserInfo(req, res) { + try { + const { userId } = req + + const user = await User.getById(userId) + + const userNetwork = { + followers: user.followers, + following: user.following, + } + + res.status(200).json({ userNetwork }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function sendCodeToEmail(req, res) { + try { + const { currentEmail, newEmail } = req.body + + const user = await User.findByEmail(currentEmail) + + if (!user) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + if ( + user.codeToChangeEmail && + Date.now() - user.codeToChangeEmail.createdAt < 90000 + ) { + return res.status(400).json({ + message: "Aguarde 1 minuto para enviar um novo código", + }) + } + + const foundUserByNewEmail = await User.findByEmail(newEmail) + if (foundUserByNewEmail) { + return res.status(409).json({ + message: "E-mail já cadastrado", + }) + } + + const code = randomize("0", 6) + + await User.update(user.email, { + codeToChangeEmail: { code, createdAt: new Date() }, + }) + await sendChangeEmailCode({ name: user.name, email: newEmail, code }) + + res.status(200).json({ + message: "Enviamos um código de confirmação para o seu e-mail", + }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function changeEmail(req, res) { + try { + const { currentEmail, newEmail, code } = req.body + + const user = await User.findByEmail(currentEmail) + if (!user) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + if (user.codeToChangeEmail && user.codeToChangeEmail.code !== code) { + return res.status(400).json({ + message: "Código inválido", + }) + } + + await User.updateEmail(user.email, newEmail) + + res.status(200).json({ message: "E-mail alterado" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function changePassword(req, res) { + try { + const { userId } = req + const { currentPassword, newPassword } = req.body + + const user = await User.getById(userId) + + if (!user) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + const isPasswordMatch = await bcrypt.compare(currentPassword, user.password) + + if (!isPasswordMatch) { + return res.status(400).json({ message: "Senha atual incorreta" }) + } + + const hashedPassword = await bcrypt.hash(newPassword, 10) + + await User.updateById(userId, { password: hashedPassword }) + + res.status(200).json({ message: "Senha alterada" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function removeAccount(req, res) { + try { + const { userId } = req + const cookies = req.cookies + + const user = await User.getById(userId) + + if (!user) { + return res.status(404).json({ message: "Usuário não encontrado" }) + } + + await Post.removeAllPostsFromUser(userId) + await Comment.removeAllFromUser(userId) + await Application.removeAllApplicationsFromUser(userId) + await Like.removeAllLikesFromUser(userId) + await Message.removeAllMessagesFromUser(userId) + await Conversation.removeAllConversationsFromUser(userId) + await User.deleteUser(userId) + + if (cookies && cookies["rf-jwt"]) { + res.clearCookie("rf-jwt", { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }) + } + + res.status(200).json({ message: "Conta removida" }) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} + +export async function getSuggestions(req, res) { + try { + const { userId } = req + const countUsers = await User.countAllUsers() + if (countUsers <= 1) { + return res.status(200).json([]) + } + + const suggestions = await User.listUserNotFollowing({ userId, limit: 5 }) + + res.status(200).json(suggestions) + } catch (error) { + console.log(error) + res.status(500).json({ message: "Erro interno do servidor" }) + } +} diff --git a/src/controllers/vacancy.controller.js b/src/controllers/vacancy.controller.js new file mode 100644 index 0000000..c2381c8 --- /dev/null +++ b/src/controllers/vacancy.controller.js @@ -0,0 +1,318 @@ +// Models +import * as Vacancy from "../models/vacancy.model.js" +import * as ApplicationVacancy from "../models/applicationVacancy.model.js" +import * as User from "../models/user.model.js" + +export async function createVacancy(req, res) { + try { + const { userId } = req + const { + occupation, + company, + description, + typeLocation, + stateUf, + city, + occupationArea, + skills, + employmentType, + contractType, + selectiveProcessAccessibility, + jobAccessibility, + accommodationAccessibility, + } = req.body + + if (typeLocation === "onsite" && (!stateUf || !city)) { + return res.status(400).send({ + message: "Estado e cidade são obrigatórios para vagas presenciais", + }) + } + + let vacancyBody = { + author: userId, + occupation, + company, + description, + typeLocation, + stateUf, + city, + occupationArea, + skills, + employmentType, + contractType, + selectiveProcessAccessibility, + jobAccessibility, + accommodationAccessibility, + } + + if (typeLocation === "remote") { + vacancyBody["stateUf"] = null + vacancyBody["city"] = null + } + + const vacancyId = await Vacancy.create(vacancyBody) + + res.status(201).json({ message: "Vaga cadastrada", vacancyId }) + } catch (error) { + console.log("Controller - Erro ao criar vaga: ", error) + res.status(500).send({ message: "Erro ao criar vaga" }) + } +} + +export async function createExternalVacancy(req, res) { + try { + const { + occupation, + company, + description, + externalVacancyLink, + externalVacancyLocation, + externalVacancyId, + } = req.body + + let vacancyBody = { + occupation, + company, + description, + externalVacancy: true, + externalVacancyLink, + externalVacancyLocation, + externalVacancyId, + typeLocation: "onsite", + } + + const vacancyId = await Vacancy.create(vacancyBody) + + res.status(201).json({ message: "Vaga cadastrada", vacancyId }) + } catch (error) { + console.log("Controller - Erro ao criar vaga externa: ", error) + res.status(500).send({ message: "Erro ao criar vaga externa" }) + } +} + +export async function editVacancy(req, res) { + try { + const { userId } = req + const { + vacancyId, + occupation, + company, + description, + typeLocation, + stateUf, + city, + occupationArea, + skills, + employmentType, + contractType, + selectiveProcessAccessibility, + jobAccessibility, + accommodationAccessibility, + } = req.body + + const foundedVacancy = await Vacancy.getVacancyById(vacancyId) + + if (!foundedVacancy) { + return res.status(404).send({ message: "Vaga não encontrada" }) + } + + if (typeLocation === "onsite" && (!stateUf || !city)) { + return res.status(400).send({ + message: "Estado e cidade são obrigatórios para vagas presenciais", + }) + } + + let vacancyBody = { + author: userId, + occupation, + company, + description, + typeLocation, + stateUf, + city, + occupationArea, + skills, + employmentType, + contractType, + selectiveProcessAccessibility, + jobAccessibility, + accommodationAccessibility, + } + + if (typeLocation === "remote") { + vacancyBody["stateUf"] = null + vacancyBody["city"] = null + } + + await Vacancy.edit(vacancyId, vacancyBody) + + res.status(201).json({ message: "Vaga editada com sucesso" }) + } catch (error) { + console.log("Controller - Erro ao criar vaga: ", error) + res.status(500).send({ message: "Erro ao criar vaga" }) + } +} + +export async function listVacancies(req, res) { + try { + const { page = 1, filters, keyword = "" } = req.body + const vacancies = await Vacancy.listAll({ page, filters, keyword }) + + res.status(200).json(vacancies) + } catch (error) { + console.log("Controller - Erro ao listar todas as vagas: ", error) + res.status(500).send({ message: "Erro ao listar todas as vagas" }) + } +} + +export async function listVacanciesByAuthor(req, res) { + try { + const { userId } = req + const { page = 1, limit = 20, filters = {}, keyword = "" } = req.body + const vacancies = await Vacancy.listAll({ + userId, + page, + limit, + filters, + keyword, + }) + + res.status(200).json(vacancies) + } catch (error) { + console.log("Controller - Erro ao listar todas as vagas: ", error) + res.status(500).send({ message: "Erro ao listar todas as vagas" }) + } +} + +export async function getVacancyInfo(req, res) { + try { + const { vacancyId } = req.params + const vacancy = await Vacancy.getVacancyById(vacancyId) + + if (!vacancy) { + return res.status(404).send({ message: "Vaga não encontrada" }) + } + + res.status(200).json(vacancy) + } catch (error) { + console.log("Controller - Erro ao buscar vaga: ", error) + res.status(500).send({ message: "Erro ao buscar vaga" }) + } +} + +export async function applyVacancy(req, res) { + try { + const { userId } = req + const { vacancyId, contactEmail } = req.body + + const foundedVacancy = await Vacancy.getVacancyById(vacancyId) + + if (!foundedVacancy) { + return res.status(404).send({ message: "Vaga não encontrada" }) + } + + const foundedUser = await User.getById(userId) + + if (!foundedUser) { + return res.status(404).send({ message: "Usuário não encontrado" }) + } + + const application = await ApplicationVacancy.create({ + candidate: userId, + vacancy: vacancyId, + contactEmail, + }) + + await Vacancy.addApplicationToVacancy(vacancyId, application._id) + + res.status(201).json({ message: "Candidatura enviada" }) + } catch (error) { + console.log("Controller - Erro aplicar para a vaga: ", error) + res.status(500).send({ message: "Erro aplicar para a vaga" }) + } +} + +export async function getVacancyCandidates(req, res) { + try { + const { page = 1, vacancyId } = req.body + const vacancy = await Vacancy.getVacancyById(vacancyId) + + if (!vacancy) { + return res.status(404).send({ message: "Vaga não encontrada" }) + } + + const { applications, totalApplications } = + await Vacancy.listCandidatesByVacancy(vacancyId, page) + + res.status(200).json({ applications, totalApplications }) + } catch (error) { + console.log("Controller - Erro ao buscar candidatos da vaga: ", error) + res.status(500).send({ message: "Erro ao buscar candidatos da vaga" }) + } +} + +export async function deleteVacancy(req, res) { + try { + const { vacancyId } = req.params + + const foundedVacancy = await Vacancy.getVacancyById(vacancyId) + + if (!foundedVacancy) { + return res.status(404).send({ message: "Vaga não encontrada" }) + } + + await ApplicationVacancy.deleteApplication(vacancyId) + + await Vacancy.removeVacancy(vacancyId) + + res.status(200).json({ message: "Vaga deletada com sucesso" }) + } catch (error) { + console.log("Controller - Erro ao buscar aplicações do usuário: ", error) + res.status(500).send({ message: "Erro ao buscar aplicações do usuário" }) + } +} + +export async function listApplicationsByUser(req, res) { + try { + const { userId } = req + const { page = 1, keyword = "" } = req.body + const { applications, totalApplications } = + await ApplicationVacancy.listByUser({ + userId, + page, + keyword, + }) + + res.status(200).json({ applications, totalApplications }) + } catch (error) { + console.log("Controller - Erro ao buscar aplicações do usuário: ", error) + res.status(500).send({ message: "Erro ao buscar aplicações do usuário" }) + } +} + +export async function getExternalVacancyById(req, res) { + try { + const { id } = req.params + const vacancy = await Vacancy.getExternalVacancy(id) + + if (!vacancy) { + return res.status(200).json({ vacancyAlreadyExist: false }) + } + + res.status(200).json({ vacancyAlreadyExist: true }) + } catch (error) { + console.log("Controller - Erro ao buscar vaga externa: ", error) + res.status(500).send({ message: "Erro ao buscar vaga externa" }) + } +} + +export async function removeExternalVacancies(req, res) { + try { + await Vacancy.removeManyVacancies() + + res.status(200).json({ message: "Vagas externas deletadas com sucesso" }) + } catch (error) { + console.log("Controller - Erro ao deletar vagas externas: ", error) + res.status(500).send({ message: "Erro ao deletar vagas externas" }) + } +} diff --git a/src/database/mongo.js b/src/database/mongo.js new file mode 100644 index 0000000..c4b1098 --- /dev/null +++ b/src/database/mongo.js @@ -0,0 +1,13 @@ +import { connect } from "mongoose" +import dotenv from "dotenv" + +dotenv.config() + +export async function mongoConnect() { + try { + await connect(process.env.MONGO_URL) + console.log(`Servidor rodando em http://localhost:${process.env.PORT}`) + } catch (error) { + console.log("Erro ao conectar com o MongoDB: ", error) + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..e53d093 --- /dev/null +++ b/src/index.js @@ -0,0 +1,37 @@ +import express from "express" +import cors from "cors" +import dotenv from "dotenv" +import cookieParser from "cookie-parser" + +// Mongo +import { mongoConnect } from "./database/mongo.js" + +// Configs +import { corsOptions } from "./config/corsOptions.js" + +// Middlewares +import { credentials } from "./middlewares/credentials.js" + +// Routes +import routes from "./routes/index.js" + +// app +import { app, server } from "./socket/socket.js" + +dotenv.config() + +mongoConnect() + +app.use(credentials) + +app.use(cookieParser()) +app.use(cors(corsOptions)) +app.use(express.urlencoded({ extended: true })) +app.use(express.json()) + +app.use("/", routes) +app.use((req, res) => { + res.status(404).json({ message: "Endpoint não encontrado" }) +}) + +server.listen(process.env.PORT) diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js new file mode 100644 index 0000000..08fb169 --- /dev/null +++ b/src/middlewares/auth.js @@ -0,0 +1,27 @@ +import jwt from "jsonwebtoken" + +export function authenticate(req, res, next) { + let success = false + + if (req.headers.authorization) { + const [authType, token] = req.headers.authorization.split(" ") + if (authType === "Bearer") { + try { + const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET) + + if (typeof decoded === "object") { + req.userId = decoded._id + } + success = true + } catch (error) { + console.log(error) + } + } + } + + if (success) { + next() + } else { + res.status(401).json({ message: "Não autorizado" }) + } +} diff --git a/src/middlewares/cloudinary.js b/src/middlewares/cloudinary.js new file mode 100644 index 0000000..3d30d39 --- /dev/null +++ b/src/middlewares/cloudinary.js @@ -0,0 +1,12 @@ +import { v2 as cloudinary } from "cloudinary" +import dotenv from "dotenv" + +dotenv.config() + +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_CLOUD_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, +}) + +export default cloudinary diff --git a/src/middlewares/credentials.js b/src/middlewares/credentials.js new file mode 100644 index 0000000..547fa07 --- /dev/null +++ b/src/middlewares/credentials.js @@ -0,0 +1,9 @@ +import { allowedOrigins } from "../config/allowedOrigins.js" + +export const credentials = (req, res, next) => { + const origin = req.headers.origin + if (allowedOrigins.includes(origin)) { + res.setHeader("Access-Control-Allow-Credentials", true) + } + next() +} diff --git a/src/middlewares/requiredParams.js b/src/middlewares/requiredParams.js new file mode 100644 index 0000000..d59a8e1 --- /dev/null +++ b/src/middlewares/requiredParams.js @@ -0,0 +1,16 @@ +import { setErrorMessage } from "../utils/index.js" + +export function requiredParams(params) { + return (request, response, next) => { + const verify = [...params].filter((key) => !request.body[key]) + if (verify.length > 0) { + return response.status(400).send({ + errors: [ + setErrorMessage(400, `Missing body params: ${verify.join(", ")}`), + ], + }) + } + + next() + } +} diff --git a/src/middlewares/upload.js b/src/middlewares/upload.js new file mode 100644 index 0000000..91e3f9b --- /dev/null +++ b/src/middlewares/upload.js @@ -0,0 +1,19 @@ +import multer from "multer" + +export const upload = multer({ + dest: "./tmp", + fileFilter: (req, file, cb) => { + const allowedMimes = [ + "image/jpeg", + "image/jpg", + "image/png", + "application/pdf", + ] + if (!allowedMimes.includes(file.mimetype)) { + cb(new Error("Formato de arquivo inválido.")) + return + } + + cb(null, true) + }, +}) diff --git a/src/middlewares/verifyAccesstoken.js b/src/middlewares/verifyAccesstoken.js new file mode 100644 index 0000000..4735132 --- /dev/null +++ b/src/middlewares/verifyAccesstoken.js @@ -0,0 +1,24 @@ +import jwt from "jsonwebtoken" +import dotenv from "dotenv" + +dotenv.config() + +export async function verifyAccessToken(req, res, next) { + const authHeader = req.headers.authorization + if (!authHeader) { + return res.status(401).json({ message: "Não autorizado" }) + } + + const accessToken = authHeader.split(" ")[1] + + jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET, (err, decoded) => { + if (err) { + console.log(err) + return res.status(401).json({ message: "Acesso não autorizado" }) + } + + req.userEmail = decoded.email + req.userId = decoded._id + next() + }) +} diff --git a/src/models/applicationVacancy.model.js b/src/models/applicationVacancy.model.js new file mode 100644 index 0000000..dc011c2 --- /dev/null +++ b/src/models/applicationVacancy.model.js @@ -0,0 +1,84 @@ +import { Schema, model } from "mongoose" + +const applicationVacancySchema = new Schema( + { + candidate: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + vacancy: { + type: Schema.Types.ObjectId, + ref: "Vacancy", + required: true, + }, + contactEmail: { + type: String, + required: true, + }, + }, + { + timestamps: true, + }, +) + +const ApplicationVacancy = model("ApplicationVacancy", applicationVacancySchema) + +export async function create(applicationVacancy) { + try { + const applicationCreated = await ApplicationVacancy.create( + applicationVacancy, + ) + return applicationCreated + } catch (error) { + console.log("DB - Erro ao criar aplicação:", error) + throw new Error(error) + } +} + +export async function listByUser({ userId, page = 1, limit = 10 }) { + try { + const applications = await ApplicationVacancy.find({ + candidate: userId, + }) + .limit(limit) + .skip((page - 1) * limit) + .populate({ path: "vacancy", populate: { path: "skills" } }) + + const totalApplications = await ApplicationVacancy.countDocuments({ + candidate: userId, + }) + + return { applications, totalApplications } + } catch (error) { + console.log("DB - Erro ao listar aplicações:", error) + throw new Error(error) + } +} + +export async function getTotalApplicationsByVacancy(vacancyId) { + try { + return await ApplicationVacancy.countDocuments({ vacancy: vacancyId }) + } catch (error) { + console.log("DB - Erro ao buscar total de aplicações:", error) + throw new Error(error) + } +} + +export async function deleteApplication(vacancyId) { + try { + await ApplicationVacancy.deleteMany({ vacancy: vacancyId }) + } catch (error) { + console.log("DB - Erro ao deletar aplicação:", error) + throw new Error(error) + } +} + +export async function removeAllApplicationsFromUser(userId) { + try { + await ApplicationVacancy.deleteMany({ candidate: userId }) + } catch (error) { + console.log("DB - Erro ao remover aplicações:", error) + throw new Error(error) + } +} diff --git a/src/models/comment.model.js b/src/models/comment.model.js new file mode 100644 index 0000000..dd11aa2 --- /dev/null +++ b/src/models/comment.model.js @@ -0,0 +1,91 @@ +import { Schema, model } from "mongoose" + +const commentSchema = new Schema( + { + content: { + type: String, + required: true, + }, + post: { + type: Schema.Types.ObjectId, + ref: "Post", + required: true, + }, + user: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + }, + { + timestamps: true, + }, +) + +const Comment = model("Comment", commentSchema) + +export async function createComment({ post, user, content }) { + try { + const commentCreated = await Comment.create({ post, user, content }) + return commentCreated + } catch (error) { + console.log("DB - Erro ao criar like:", error) + throw new Error(error) + } +} + +export async function getComments(postId, page) { + try { + const itemsPerPage = 5 + const comments = await Comment.find({ post: postId }) + .lean() + .populate("user", ["name", "avatar", "headline"]) + .sort({ createdAt: -1 }) + .limit(itemsPerPage) + .skip((page - 1) * itemsPerPage) + + const totalComments = await Comment.countDocuments({ post: postId }) + const totalPages = Math.ceil(totalComments / itemsPerPage) + + return { comments, totalPages } + } catch (error) { + console.log("DB - Erro ao buscar comentários:", error) + throw new Error(error) + } +} + +export async function updateComment(commentId, content) { + try { + await Comment.updateOne({ _id: commentId }, { content }) + } catch (error) { + console.log("DB - Erro ao editar comentário:", error) + throw new Error(error) + } +} + +export async function deleteComment(commentId) { + try { + await Comment.findOneAndDelete({ _id: commentId }) + } catch (error) { + console.log("DB - Erro ao deletar comentário:", error) + throw new Error(error) + } +} + +export async function removeManyComments(postId) { + try { + await Comment.deleteMany({ post: postId }) + } catch (error) { + console.log("DB - Erro ao deletar comentários:", error) + throw new Error(error) + } +} + +export async function removeAllFromUser(userId) { + try { + await Comment.deleteMany({ user: userId }) + } catch (error) { + console.log("DB - Erro ao deletar comentários:", error) + throw new Error(error) + } +} diff --git a/src/models/community.model.js b/src/models/community.model.js new file mode 100644 index 0000000..3f6aab9 --- /dev/null +++ b/src/models/community.model.js @@ -0,0 +1,182 @@ +import _ from "lodash" +import { Schema, model } from "mongoose" + +const communitySchema = new Schema( + { + name: { + type: String, + required: true, + }, + author: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + description: { + type: String, + default: null, + }, + formattedLink: { + type: String, + required: true, + }, + link: { + type: String, + required: true, + }, + rating: { + type: Number, + default: 0, + }, + totalRatings: { + type: Number, + default: 0, + }, + platform: { + type: String, + required: true, + enum: [ + "whatsapp", + "telegram", + "discord", + "facebook", + "linkedin", + "reddit", + "others", + ], + }, + ratedUsers: { + type: [Schema.Types.ObjectId], + ref: "User", + default: [], + }, + professionalArea: { + type: Schema.Types.ObjectId, + ref: "ProfessionalArea", + required: true, + }, + skills: { + type: [Schema.Types.ObjectId], + ref: "Skill", + default: [], + }, + }, + { + timestamps: true, + }, +) + +const CommunityModel = model("Community", communitySchema) + +export function create(community) { + try { + return CommunityModel.create(community) + } catch (error) { + console.log("DB - Erro ao criar uma nova comunidade: ", error) + throw new Error(error) + } +} + +export async function list({ + userId, + page = 1, + limit = 10, + filters = {}, + keyword, +}) { + try { + let queryObject = {} + let sortQuery = { createdAt: -1 } + + if (!_.isEmpty(filters)) { + if (filters.professionalArea) { + queryObject.professionalArea = filters.professionalArea + } + + if (filters.skills && filters.skills.length > 0) { + queryObject.skills = { $in: filters.skills } + } + + if (filters.platforms && filters.platforms.length > 0) { + queryObject.platform = { $in: filters.platforms } + } + + if (filters.sortType === "relevance") { + sortQuery = { rating: -1 } + } + } + + if (keyword) { + queryObject = { + ...queryObject, + name: { $regex: keyword, $options: "i" }, + } + } + + if (userId) { + queryObject.author = userId + } + + const communities = await CommunityModel.find(queryObject) + .populate("professionalArea", "name") + .populate("skills", "name") + .limit(limit) + .skip((page - 1) * limit) + .sort(sortQuery) + + const totalCommunities = await CommunityModel.countDocuments(queryObject) + + return { communities, totalCommunities } + } catch (error) { + console.log("DB - Erro ao buscar comunidades: ", error) + throw new Error(error) + } +} + +export async function getCommunityById(communityId) { + try { + return await CommunityModel.findById(communityId) + } catch (error) { + console.log("DB - Erro ao buscar comunidade por id: ", error) + throw new Error(error) + } +} + +export async function getCommunityByFormattedLink(formattedLink) { + try { + return await CommunityModel.findOne({ formattedLink }) + } catch (error) { + console.log("DB - Erro ao buscar comunidade por id: ", error) + throw new Error(error) + } +} + +export async function update(communityId, community) { + try { + await CommunityModel.updateOne({ _id: communityId }, community) + } catch (error) { + console.log("DB - Erro ao buscar comunidade por id: ", error) + throw new Error(error) + } +} + +export async function updateRating({ userId, communityId, rating }) { + try { + await CommunityModel.updateOne( + { _id: communityId }, + { $inc: { rating, totalRatings: 1 }, $push: { ratedUsers: userId } }, + ) + } catch (error) { + console.log("DB - Erro ao buscar comunidade por id: ", error) + throw new Error(error) + } +} + +export async function remove(communityId) { + try { + await CommunityModel.deleteOne({ _id: communityId }) + } catch (error) { + console.log("DB - Erro ao buscar comunidade por id: ", error) + throw new Error(error) + } +} diff --git a/src/models/conversation.model.js b/src/models/conversation.model.js new file mode 100644 index 0000000..af83a5e --- /dev/null +++ b/src/models/conversation.model.js @@ -0,0 +1,146 @@ +import { Schema, model } from "mongoose" + +const conversationSchema = new Schema( + { + participants: [ + { + type: Schema.Types.ObjectId, + ref: "User", + }, + ], + messages: [ + { + type: Schema.Types.ObjectId, + ref: "Message", + default: [], + }, + ], + }, + { + timestamps: true, + }, +) + +const ConversationModel = model("Conversation", conversationSchema) + +export async function create(conversationBody) { + try { + const newConversation = await ConversationModel.create(conversationBody) + return newConversation + } catch (error) { + throw new Error(error) + } +} + +export async function getConversationByParticipants({ + userId, + receiverId, + withPopulate = false, +}) { + try { + let conversation = null + if (withPopulate) { + conversation = await ConversationModel.findOne({ + participants: { + $all: [userId, receiverId], + $size: 2, + }, + }).populate("messages") + } else { + conversation = await ConversationModel.findOne({ + participants: { + $all: [userId, receiverId], + $size: 2, + }, + }) + } + + return conversation + } catch (error) { + throw new Error(error) + } +} + +export async function getConversationById( + conversationId, + page = 1, + limit = 10, +) { + try { + const conversation = await ConversationModel.findById(conversationId) + + const totalPages = Math.ceil(conversation.messages.length / limit) + + await conversation.populate({ + path: "messages", + options: { sort: { createdAt: -1 }, limit, skip: (page - 1) * limit }, + populate: [ + { path: "sender", select: "name avatar" }, + { path: "receiver", select: "name avatar" }, + ], + }) + + const conversationBody = { + messages: conversation.messages, + totalPages, + } + return conversationBody + } catch (error) { + throw new Error(error) + } +} + +export async function addMessage(conversationId, messageId) { + try { + await ConversationModel.findByIdAndUpdate(conversationId, { + $push: { messages: messageId }, + }) + } catch (error) { + throw new Error(error) + } +} + +export async function listByUser(userId, keyword) { + try { + const conversations = await ConversationModel.find({ + participants: { $in: [userId] }, + }).populate([ + { + path: "participants", + select: "name avatar headline", + }, + { + path: "messages", + options: { perDocumentLimit: 1, sort: { createdAt: -1 } }, + select: "content sender createdAt", + }, + ]) + + let filteredConversations = null + if (keyword) { + filteredConversations = conversations.filter((conversation) => { + const filteredParticipants = conversation.participants.filter( + (participant) => + participant._id !== userId && + participant.name.toLowerCase().includes(keyword.toLowerCase()), + ) + + return filteredParticipants.length > 0 + }) + } else { + filteredConversations = conversations + } + + return filteredConversations + } catch (error) { + throw new Error(error) + } +} + +export async function removeAllConversationsFromUser(userId) { + try { + await ConversationModel.deleteMany({ participants: { $in: [userId] } }) + } catch (error) { + throw new Error(error) + } +} diff --git a/src/models/like.model.js b/src/models/like.model.js new file mode 100644 index 0000000..ae7dcaa --- /dev/null +++ b/src/models/like.model.js @@ -0,0 +1,58 @@ +import { Schema, model } from "mongoose" + +const likeSchema = new Schema( + { + post: { + type: Schema.Types.ObjectId, + ref: "Post", + required: true, + }, + user: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + }, + { + timestamps: true, + }, +) + +const Like = model("Like", likeSchema) + +export async function newLike(post, user) { + try { + const likeCreated = await Like.create({ post, user }) + return likeCreated + } catch (error) { + console.log("DB - Erro ao criar like:", error) + throw new Error(error) + } +} + +export async function removeLike(postId, userId) { + try { + await Like.findOneAndDelete({ post: postId, user: userId }) + } catch (error) { + console.log("DB - Erro ao remover like:", error) + throw new Error(error) + } +} + +export async function removeManyLikes(postId) { + try { + await Like.deleteMany({ post: postId }) + } catch (error) { + console.log("DB - Erro ao remover likes:", error) + throw new Error(error) + } +} + +export async function removeAllLikesFromUser(userId) { + try { + await Like.deleteMany({ user: userId }) + } catch (error) { + console.log("DB - Erro ao remover likes:", error) + throw new Error(error) + } +} diff --git a/src/models/message.model.js b/src/models/message.model.js new file mode 100644 index 0000000..fe0b864 --- /dev/null +++ b/src/models/message.model.js @@ -0,0 +1,47 @@ +import { Schema, model } from "mongoose" + +const messageSchema = new Schema( + { + sender: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + receiver: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + content: { + type: String, + required: true, + }, + }, + { + timestamps: true, + }, +) + +const MessageModel = model("Message", messageSchema) + +export async function create(messageBody) { + try { + const newMessage = await MessageModel.create(messageBody) + const newMessagePopulated = await newMessage.populate( + "sender", + "name avatar", + ) + + return newMessagePopulated + } catch (error) { + throw new Error(error) + } +} + +export async function removeAllMessagesFromUser(userId) { + try { + await MessageModel.deleteMany({ sender: userId }) + } catch (error) { + throw new Error(error) + } +} diff --git a/src/models/post.model.js b/src/models/post.model.js new file mode 100644 index 0000000..6e78c0b --- /dev/null +++ b/src/models/post.model.js @@ -0,0 +1,187 @@ +import { Schema, model } from "mongoose" + +const postSchema = new Schema( + { + author: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + content: { + type: String, + required: true, + }, + media: { + type: String, + default: null, + }, + mediaDescription: { + type: String, + default: null, + }, + cloudinaryId: { + type: String, + default: null, + }, + likes: [ + { + type: Schema.Types.ObjectId, + ref: "Like", + }, + ], + comments: [ + { + type: Schema.Types.ObjectId, + ref: "Comment", + }, + ], + }, + { + timestamps: true, + }, +) + +const Post = model("Post", postSchema) + +export async function newPost({ + author, + content, + media = null, + mediaDescription = null, + cloudinaryId = null, +}) { + try { + const postCreated = await Post.create({ + author, + content, + media, + mediaDescription, + cloudinaryId, + }) + return postCreated.populate("author", ["name", "avatar", "headline"]) + } catch (error) { + console.log("DB - Erro ao criar postagem:", error) + throw new Error(error) + } +} + +export async function updatePost(postId, postBody) { + try { + const postUpdated = await Post.findOneAndUpdate( + { _id: postId }, + { $set: postBody }, + { returnDocument: "after" }, + ) + + return postUpdated.populate("author", ["name", "avatar"]) + } catch (error) { + console.log("DB - Erro ao atualizar postagem:", error) + throw new Error(error) + } +} + +export async function getAllPosts() { + try { + const posts = await Post.find().populate("author", ["name", "avatar"]) + // .populate("comments") + // .populate("likes") + + return posts + } catch (error) { + console.log("DB - Erro ao buscar todas as postagens:", error) + throw new Error(error) + } +} + +export async function getPostById(postId) { + try { + const post = await Post.findById(postId).populate("author", [ + "name", + "avatar", + ]) + + return post + } catch (error) { + console.log("DB - Erro ao buscar post:", error) + throw new Error(error) + } +} + +export async function addLikePost(postId, userId) { + try { + await Post.updateOne({ _id: postId }, { $push: { likes: userId } }) + } catch (error) { + console.log("DB - Erro ao curtir post:", error) + throw new Error(error) + } +} + +export async function removeLikePost(postId, userId) { + try { + await Post.updateOne({ _id: postId }, { $pull: { likes: userId } }) + } catch (error) { + console.log("DB - Erro ao remover post:", error) + throw new Error(error) + } +} + +export async function addCommentPost(postId, commentId) { + try { + await Post.updateOne( + { _id: postId }, + { $push: { comments: commentId } }, + { returnDocument: "after" }, + ) + } catch (error) { + console.log("DB - Erro ao adicionar comentário:", error) + throw new Error(error) + } +} + +export async function deletePostComment(postId, commentId) { + try { + await Post.updateOne({ _id: postId }, { $pull: { comments: commentId } }) + } catch (error) { + console.log("DB - Erro ao remover comentário:", error) + throw new Error(error) + } +} + +export async function removePost(postId) { + try { + await Post.deleteOne({ _id: postId }) + } catch (error) { + console.log("DB - Erro ao remover postagem:", error) + throw new Error(error) + } +} + +export async function removeAllPostsFromUser(userId) { + try { + await Post.deleteMany({ author: userId }) + } catch (error) { + console.log("DB - Erro ao remover todas as postagens do usuário:", error) + throw new Error(error) + } +} + +export async function feedRandom() { + try { + const posts = await Post.aggregate([ + { $sample: { size: 25 } }, + { + $lookup: { + from: "users", // Nome da coleção de autores + localField: "author", // Campo na coleção postagens que se refere ao autor + foreignField: "_id", // Campo na coleção autores que corresponde ao campo localField + as: "author", // Nome do campo que conterá os dados do autor + }, + }, + { $unwind: "$author" }, // Desnormaliza o array resultante + ]) + return posts + } catch (error) { + console.error("Erro ao recuperar postagens aleatórias paginadas:", error) + return null + } +} diff --git a/src/models/professionalArea.model.js b/src/models/professionalArea.model.js new file mode 100644 index 0000000..482be39 --- /dev/null +++ b/src/models/professionalArea.model.js @@ -0,0 +1,31 @@ +import { Schema, model } from "mongoose" + +const professionalAreaSchema = new Schema({ + name: { + type: String, + required: true, + unique: true, + }, +}) + +const ProfessionalArea = model("ProfessionalArea", professionalAreaSchema) + +export async function createProfessionalArea(professionalAreaName) { + try { + await ProfessionalArea.create({ name: professionalAreaName }) + } catch (error) { + console.log("DB - Erro ao criar área profissional: ", error) + throw new Error(error) + } +} + +export async function getAll() { + try { + const professionalAreas = await ProfessionalArea.find().sort({ name: 1 }) + + return professionalAreas + } catch (error) { + console.log("DB - Erro ao listar áreas profissionais: ", error) + throw new Error(error) + } +} diff --git a/src/models/skill.model.js b/src/models/skill.model.js new file mode 100644 index 0000000..e19be91 --- /dev/null +++ b/src/models/skill.model.js @@ -0,0 +1,50 @@ +import { Schema, model } from "mongoose" + +const skillSchema = new Schema({ + name: { + type: String, + required: true, + }, + professionalArea: { + type: Schema.Types.ObjectId, + ref: "ProfessionalArea", + }, +}) + +const Skill = model("Skill", skillSchema) + +export async function createSkill(skillName, professionalAreaId) { + try { + await Skill.create({ + name: skillName, + professionalArea: professionalAreaId, + }) + } catch (error) { + console.log("DB - Erro ao criar skill: ", error) + throw new Error(error) + } +} + +export async function createManySkill(skills, professionalAreaId) { + try { + const skillList = skills.map((skill) => ({ + name: skill, + professionalArea: professionalAreaId, + })) + await Skill.insertMany(skillList) + } catch (error) { + console.log("DB - Erro ao criar skill: ", error) + throw new Error(error) + } +} + +export async function getByProfessionalArea(professionalAreaId) { + try { + const skills = await Skill.find({ professionalArea: professionalAreaId }) + + return skills + } catch (error) { + console.log("DB - Erro ao listar skill: ", error) + throw new Error(error) + } +} diff --git a/src/models/user.model.js b/src/models/user.model.js new file mode 100644 index 0000000..dc5e41c --- /dev/null +++ b/src/models/user.model.js @@ -0,0 +1,758 @@ +import mongoose, { Schema, model } from "mongoose" + +const userSchema = new Schema( + { + name: { + type: String, + required: true, + }, + avatar: { + type: String, + default: null, + }, + avatarCloudinaryId: { + type: String, + default: null, + }, + resumeUrl: { + type: String, + default: null, + }, + headline: { + type: String, + default: null, + }, + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + }, + password: { + type: String, + required: true, + trim: true, + select: false, + }, + profileType: { + type: String, + required: true, + enum: ["person", "company"], + }, + city: { + type: String, + }, + stateUf: { + type: String, + }, + occupationArea: { + type: String, + }, + companyType: { + type: String, + }, + website: { + type: String, + }, + active: { + type: Boolean, + default: false, + }, + following: [ + { + type: Schema.Types.ObjectId, + ref: "User", + }, + ], + followers: [ + { + type: Schema.Types.ObjectId, + ref: "User", + }, + ], + feed: [ + { + type: Schema.Types.ObjectId, + ref: "Post", + }, + ], + about: { + type: String, + default: null, + }, + experience: [ + { + occupation: { + type: String, + required: true, + }, + company: { + type: String, + required: true, + }, + startDateMonth: { + type: String, + required: true, + }, + startDateYear: { + type: String, + required: true, + }, + endDateMonth: { + type: String, + default: null, + }, + endDateYear: { + type: String, + default: null, + }, + current: { + type: Boolean, + required: true, + }, + type: { + type: String, + required: true, + enum: [ + "Tempo integral", + "Meio período", + "Autônomo", + "Estágio", + "Freelancer", + "Trainee", + "Aprendiz", + "Voluntário", + "Terceirizado", + ], + }, + description: { + type: String, + }, + createdAt: { + type: Date, + }, + }, + ], + academicEducation: [ + { + name: { + type: String, + required: true, + }, + institution: { + type: String, + required: true, + }, + degree: { + type: String, + required: true, + enum: [ + "Ensino fundamental", + "Ensino médio", + "Técnico", + "Tecnólogo", + "Graduação", + "Pós-graduação", + "Mestrado", + "Doutorado", + "Pós-doutorado", + ], + }, + startDateMonth: { + type: String, + required: true, + }, + startDateYear: { + type: String, + required: true, + }, + endDateMonth: { + type: String, + default: null, + }, + endDateYear: { + type: String, + default: null, + }, + }, + ], + certificates: [ + { + name: { + type: String, + required: true, + }, + institution: { + type: String, + required: true, + }, + issueMonth: { + type: String, + required: true, + }, + issueYear: { + type: String, + required: true, + }, + url: { + type: String, + required: true, + }, + }, + ], + refreshToken: { + type: String, + select: false, + }, + emailToken: { + type: String, + select: false, + }, + forgotPasswordToken: { + type: String, + select: false, + }, + codeToChangeEmail: { + type: { code: String, createdAt: Date }, + default: null, + }, + }, + { + timestamps: true, + }, +) + +const User = model("User", userSchema) + +export async function listAll(userId) { + try { + const users = await User.find({ _id: { $ne: userId } }) + .limit(5) + .select(["name", "avatar", "headline"]) + + return users + } catch (error) { + console.log("DB - Erro ao listar todos os usuários: ", error) + throw new Error(error) + } +} + +export async function getById(id) { + try { + const user = await User.findById(id).select("+password") + return user + } catch (error) { + console.log("DB - Erro ao buscar usuário por ID: ", error) + throw new Error(error) + } +} + +export async function getByEmail(email) { + try { + const user = await User.findOne({ email }) + return user + } catch (error) { + console.log("DB - Erro ao buscar usuário por ID: ", error) + throw new Error(error) + } +} + +export async function findByEmail(email) { + try { + const user = User.findOne({ email }) + .select("+password") + .select("+emailToken") + .select("+forgotPasswordToken") + + return user + } catch (error) { + console.log("DB - Erro ao buscar usuário por email: ", error) + throw new Error(error) + } +} + +export async function findByRefreshToken(refreshToken) { + try { + const user = User.findOne({ refreshToken }).select("+refreshToken") + + return user + } catch (error) { + console.log("DB - Erro ao buscar usuário por refreshToken: ", error) + } +} + +export async function create(userData) { + try { + await User.create(userData) + } catch (error) { + console.log("DB - Erro ao criar usuário: ", error) + throw new Error(error) + } +} + +export async function update(email, userData) { + try { + await User.updateOne({ email }, userData) + } catch (error) { + console.log("DB - Erro ao atualizar usuário: ", error) + throw new Error(error) + } +} + +export async function updateById(userId, data) { + try { + const user = await User.findOneAndUpdate({ _id: userId }, data, { + returnDocument: "after", + }) + return user + } catch (error) { + console.log("DB - Erro ao atualizar usuário: ", error) + throw new Error(error) + } +} + +export async function updateExperience(userId, experienceData) { + try { + await User.updateOne({ _id: userId }, { experience: experienceData }) + } catch (error) { + console.log("DB - Erro ao adicionar experiência: ", error) + throw new Error(error) + } +} + +export async function updateAcademicEducation(userId, academicEducationData) { + try { + await User.updateOne( + { _id: userId }, + { academicEducation: academicEducationData }, + ) + } catch (error) { + console.log("DB - Erro ao adicionar formação acadêmica: ", error) + throw new Error(error) + } +} + +export async function updateCertificates(userId, certificatesData) { + try { + await User.updateOne({ _id: userId }, { certificates: certificatesData }) + } catch (error) { + console.log("DB - Erro ao adicionar formação acadêmica: ", error) + throw new Error(error) + } +} + +export async function getFollowers(userId) { + try { + const user = await User.findOne({ _id: userId }).populate("followers") + + return user.followers + } catch (error) { + console.log("DB - Erro ao buscar seguidores: ", error) + throw new Error(error) + } +} + +export async function addNewFollowing(userId, followingId) { + try { + await User.updateOne({ _id: userId }, { $push: { following: followingId } }) + } catch (error) { + console.log("DB - Erro ao adicionar usuário na lista de seguindo: ", error) + throw new Error(error) + } +} + +export async function addNewFollower(userId, followerId) { + try { + await User.updateOne({ _id: userId }, { $push: { followers: followerId } }) + } catch (error) { + console.log( + "DB - Erro ao adicionar usuário na lista de seguidores: ", + error, + ) + throw new Error(error) + } +} + +export async function removeFollower(userId, followerId) { + try { + await User.updateOne({ _id: userId }, { $pull: { following: followerId } }) + await User.updateOne({ _id: followerId }, { $pull: { followers: userId } }) + } catch (error) { + console.log( + "DB - Erro ao adicionar usuário na lista de seguidores: ", + error, + ) + throw new Error(error) + } +} + +export async function addInFeed(users, postId) { + try { + await User.updateMany({ _id: { $in: users } }, { $push: { feed: postId } }) + } catch (error) { + console.log("DB - Erro ao adicionar postagem no feed: ", error) + throw new Error(error) + } +} + +export async function getFeedPosts(userId, page) { + try { + const itemsPerPage = 25 + const feedData = await User.findOne({ _id: userId }) + .select({ feed: 1 }) + .lean() + .populate({ + path: "feed", + options: { + sort: { + createdAt: -1, + }, + skip: (page - 1) * itemsPerPage, + limit: itemsPerPage, + }, + populate: { + path: "author", + select: ["name", "avatar", "headline"], + model: "User", + }, + }) + + const totalItems = await User.findOne({ _id: userId }).select({ + feed: 1, + }) + const totalPages = Math.ceil(totalItems.feed.length / itemsPerPage) + + return { posts: feedData.feed, total: totalPages } + } catch (error) { + console.log("DB - Erro ao buscar todas as postagens:", error) + throw new Error(error) + } +} + +export async function removeFromFeed(userId, postId) { + try { + await User.updateOne({ _id: userId }, { $pull: { feed: postId } }) + } catch (error) { + console.log("DB - Erro ao remover postagem do feed: ", error) + throw new Error(error) + } +} + +export async function listAllFollowing({ userId, page, limit, keyword }) { + try { + let queryObject = {} + + if (keyword) { + queryObject = { + ...queryObject, + name: { $regex: keyword, $options: "i" }, + } + } + const userFounded = await getById(userId) + + const following = await User.find( + { + ...queryObject, + _id: { $in: userFounded.following }, + }, + { name: 1, avatar: 1, headline: 1, followers: 1, following: 1 }, + ) + .skip((page - 1) * limit) + .limit(limit) + + const usersBody = following.map((user) => ({ + _id: user._id, + name: user.name, + avatar: user.avatar, + headline: user.headline, + followers: user.followers.length, + following: user.following.length, + })) + + return { + users: usersBody, + total: userFounded.following.length, + } + } catch (error) { + console.log("DB - Erro ao listar todos os usuários seguidos: ", error) + throw new Error(error) + } +} + +export async function listAllFollowers({ + userId, + page = 1, + limit = 10, + keyword, +}) { + try { + let queryObject = {} + + if (keyword) { + queryObject = { + ...queryObject, + name: { $regex: keyword, $options: "i" }, + } + } + + const userFounded = await getById(userId) + + const followers = await User.find( + { + ...queryObject, + _id: { $in: userFounded.followers }, + }, + { name: 1, avatar: 1, headline: 1, followers: 1, following: 1 }, + ) + .skip((page - 1) * limit) + .limit(limit) + + const usersBody = followers.map((user) => ({ + _id: user._id, + name: user.name, + avatar: user.avatar, + headline: user.headline, + followers: user.followers.length, + following: user.following.length, + })) + + return { + users: usersBody, + total: userFounded.followers.length, + } + } catch (error) { + console.log("DB - Erro ao listar todos os usuários seguidos: ", error) + throw new Error(error) + } +} + +export async function listAllSuggestions({ userId, limit, keyword }) { + try { + let queryObject = {} + + if (keyword) { + queryObject = { + ...queryObject, + name: { $regex: keyword, $options: "i" }, + } + } + + const userFounded = await getById(userId) + const userFoundedHeadline = userFounded.headline + + const suggestions = await User.aggregate([ + { + $match: { + _id: { $nin: [userId, ...userFounded.following] }, + ...queryObject, + }, + }, + { + $addFields: { + distancia: { + $function: { + body: function (userHeadline, userFoundedHeadline) { + const levenshteinDistance = (s, t) => { + if (!t) return Infinity + if (!s) return Infinity + if (!s.length) return t.length + if (!t.length) return s.length + const arr = [] + for (let i = 0; i <= t.length; i++) { + arr[i] = [i] + for (let j = 1; j <= s.length; j++) { + arr[i][j] = + i === 0 + ? j + : Math.min( + arr[i - 1][j] + 1, + arr[i][j - 1] + 1, + arr[i - 1][j - 1] + + (s[j - 1] === t[i - 1] ? 0 : 1), + ) + } + } + return arr[t.length][s.length] + } + + const result = levenshteinDistance( + userFoundedHeadline, + userHeadline, + ) + + return result + }, + args: ["$headline", userFoundedHeadline], + lang: "js", + }, + }, + }, + }, + { $sort: { distancia: 1 } }, + { $limit: limit }, + { + $project: { + name: 1, + avatar: 1, + headline: 1, + following: 1, + followers: 1, + distancia: 1, + }, + }, + ]) + + let suggestionsBody = [] + suggestions.forEach((suggestion) => { + if (suggestion._id.toString() !== userId) { + const userBody = { + _id: suggestion._id, + name: suggestion.name, + avatar: suggestion.avatar, + headline: suggestion.headline, + followers: suggestion.followers.length, + following: suggestion.following.length, + } + suggestionsBody.push(userBody) + } + }) + + const total = await User.aggregate([ + { + $match: { + $and: [ + { _id: { $nin: [userId, ...userFounded.following] } }, + queryObject, + ], + }, + }, + { $count: "total" }, + ]) + + return { + users: suggestionsBody, + total: total, + } + } catch (error) { + console.log("DB - Erro ao listar sugestões de usuários: ", error) + throw new Error(error) + } +} + +export async function listAllUsers({ userId, page, limit, keyword }) { + try { + let queryObject = { _id: { $ne: userId } } + + if (keyword) { + queryObject = { + ...queryObject, + name: { $regex: keyword, $options: "i" }, + } + } + + const users = await User.find(queryObject) + .limit(limit) + .skip((page - 1) * limit) + .select(["name", "avatar", "headline", "followers", "following"]) + + const usersBody = users.map((user) => ({ + _id: user._id, + name: user.name, + avatar: user.avatar, + headline: user.headline, + followers: user.followers.length, + following: user.following.length, + })) + const total = await User.countDocuments(queryObject) + + return { users: usersBody, total } + } catch (error) { + console.log("DB - Erro ao listar todos os usuários: ", error) + throw new Error(error) + } +} + +export async function listUserNotFollowing({ userId, limit = 5, keyword }) { + try { + const user = await User.findById(userId) + + let queryObject = { _id: { $ne: userId, $nin: user.following } } + if (keyword) { + queryObject = { + ...queryObject, + name: { $regex: keyword, $options: "i" }, + } + } + + const users = await User.find(queryObject) + .limit(limit) + .sort({ createdAt: -1 }) + .select(["name", "avatar", "headline", "followers", "following"]) + + const usersBody = users.map((user) => ({ + _id: user._id, + name: user.name, + avatar: user.avatar, + headline: user.headline, + followers: user.followers.length, + following: user.following.length, + })) + + const total = await User.countDocuments(queryObject) + + return { users: usersBody, total } + } catch (error) { + console.log("DB - Erro ao buscar usuário por ID: ", error) + throw new Error(error) + } +} + +export async function updateEmail(currentEmail, newEmail) { + try { + await User.updateOne( + { email: currentEmail }, + { email: newEmail, codeToChangeEmail: null }, + ) + } catch (error) { + console.log("DB - Erro ao atualizar email: ", error) + throw new Error(error) + } +} + +export async function deleteUser(id) { + try { + await User.deleteOne({ _id: id }) + } catch (error) { + console.log("DB - Erro ao excluir usuário: ", error) + throw new Error(error) + } +} + +export async function getSuggestions(userId) { + try { + const suggestions = await User.aggregate([ + { $match: { _id: { $ne: new mongoose.Types.ObjectId(userId) } } }, // Exclui o próprio usuário + { $sample: { size: 5 } }, // Seleciona aleatoriamente 5 usuários + { + $match: { + followers: { $not: { $in: [new mongoose.Types.ObjectId(userId)] } }, // Exclui usuários que o usuário já segue + }, + }, + ]) + + return suggestions + } catch (error) { + console.log("DB - Erro ao buscar sugestões de usuários: ", error) + throw new Error(error) + } +} + +export async function countAllUsers() { + try { + return await User.countDocuments() + } catch (error) { + console.log("DB - Erro ao buscar quantidade de usuários: ", error) + throw new Error(error) + } +} diff --git a/src/models/vacancy.model.js b/src/models/vacancy.model.js new file mode 100644 index 0000000..ebebab5 --- /dev/null +++ b/src/models/vacancy.model.js @@ -0,0 +1,306 @@ +import _ from "lodash" +import { Schema, model } from "mongoose" + +import * as ApplicationVacancy from "./applicationVacancy.model.js" + +const vacancySchema = new Schema( + { + author: { + type: Schema.Types.ObjectId, + ref: "User", + required: false, + }, + occupation: { + type: String, + required: true, + }, + company: { + type: String, + required: true, + }, + description: { + type: String, + required: true, + }, + typeLocation: { + type: String, + required: true, + enum: ["remote", "onsite", "hybrid"], + }, + externalVacancy: { + type: Boolean, + default: false, + }, + externalVacancyLink: { + type: String, + default: null, + }, + externalVacancyLocation: { + type: String, + default: null, + }, + externalVacancyId: { + type: String, + default: null, + }, + stateUf: { + type: String, + required: false, + }, + city: { + type: String, + required: false, + }, + employmentType: { + type: String, + required: false, + enum: [ + "fullTime", + "partTime", + "autonomous", + "internship", + "freelancer", + "trainee", + "temporary", + "apprentice", + "volunteer", + "outsourced", + ], + }, + contractType: { + type: String, + required: false, + enum: ["clt", "pj", "internship", "other"], + }, + selectiveProcessAccessibility: [ + { + type: String, + required: false, + }, + ], + jobAccessibility: [ + { + type: String, + required: false, + }, + ], + accommodationAccessibility: [ + { + type: String, + required: false, + }, + ], + occupationArea: { + type: Schema.Types.ObjectId, + ref: "ProfessionalArea", + required: false, + }, + skills: [ + { + type: Schema.Types.ObjectId, + ref: "Skill", + required: false, + }, + ], + applications: [ + { + type: Schema.Types.ObjectId, + ref: "ApplicationVacancy", + }, + ], + }, + { + timestamps: true, + }, +) + +const VacancyModel = model("Vacancy", vacancySchema) + +export async function create(vacancy) { + try { + const newVacancy = await VacancyModel.create(vacancy) + return newVacancy._id + } catch (error) { + console.error("DB - Erro ao criar uma nova vaga: ", error) + throw new Error(error) + } +} + +export async function edit(vacancyId, vacancy) { + try { + await VacancyModel.updateOne({ _id: vacancyId }, vacancy) + } catch (error) { + console.error("DB - Erro ao criar uma nova vaga: ", error) + throw new Error(error) + } +} + +export async function listAll({ + userId, + page = 1, + limit = 20, + filters, + keyword, +}) { + const itemsPerPage = limit + let queryObject = {} + if (!_.isEmpty(filters)) { + if (filters.typeLocation && filters.typeLocation.length > 0) { + queryObject.typeLocation = { $in: filters.typeLocation } + } + + if (filters.contractType && filters.contractType.length > 0) { + queryObject.contractType = { $in: filters.contractType } + } + + if (filters.employmentType) { + queryObject.employmentType = filters.employmentType + } + + if (filters.occupationArea) { + queryObject.occupationArea = filters.occupationArea + } + } + + if (keyword) { + queryObject = { + ...queryObject, + $or: [ + { occupation: { $regex: keyword, $options: "i" } }, + { company: { $regex: keyword, $options: "i" } }, + ], + } + } + + if (userId) { + queryObject.author = userId + } + + try { + const vacancies = await VacancyModel.find(queryObject) + .select({ + author: 1, + occupation: 1, + company: 1, + description: 1, + typeLocation: 1, + stateUf: 1, + city: 1, + contractType: 1, + occupationArea: 1, + employmentType: 1, + skills: 1, + active: 1, + externalVacancy: 1, + externalVacancyLink: 1, + externalVacancyLocation: 1, + createdAt: 1, + updatedAt: 1, + }) + .populate("skills") + .sort({ createdAt: -1 }) + .skip((page - 1) * itemsPerPage) + .limit(itemsPerPage) + + const totalVacancies = await VacancyModel.countDocuments(queryObject) + + return { vacancies, totalVacancies } + } catch (error) { + console.error("DB - Erro ao listar todos as vagas: ") + throw new Error(error) + } +} + +export async function getVacancyById(id) { + try { + const vacancy = await VacancyModel.findById(id) + .populate("author") + .populate("skills") + .populate("occupationArea") + .populate("applications") + + return vacancy + } catch (error) { + console.error("DB - Erro ao buscar vaga por id: ", error) + throw new Error(error) + } +} + +export async function addApplicationToVacancy(vacancyId, applicationId) { + try { + await VacancyModel.updateOne( + { _id: vacancyId }, + { $push: { applications: applicationId } }, + ) + } catch (error) { + console.error("DB - Erro ao adicionar aplicação a vaga: ", error) + throw new Error(error) + } +} + +export async function listCandidatesByVacancy(vacancyId, page = 1) { + const itemsPerPage = 2 + try { + const vacancy = await VacancyModel.findById(vacancyId).populate({ + path: "applications", + options: { + skip: (page - 1) * itemsPerPage, + limit: itemsPerPage, + }, + populate: { + path: "candidate", + select: [ + "name", + "avatar", + "headline", + "resumeUrl", + "about", + "stateUf", + "city", + ], + }, + }) + + const totalApplications = + await ApplicationVacancy.getTotalApplicationsByVacancy(vacancyId) + + return { + applications: vacancy.applications, + totalApplications, + } + } catch (error) { + console.error("DB - Erro ao listar candidatos por vaga: ", error) + throw new Error(error) + } +} + +export async function removeVacancy(vacancyId) { + try { + await VacancyModel.deleteOne({ _id: vacancyId }) + } catch (error) { + console.error("DB - Erro ao deletar vaga: ", error) + throw new Error(error) + } +} + +export async function removeManyVacancies() { + try { + await VacancyModel.deleteMany({ externalVacancy: true }) + } catch (error) { + console.error("DB - Erro ao deletar todas as vagas: ", error) + throw new Error(error) + } +} + +export async function getExternalVacancy(externalVacancyId) { + try { + const vacancy = await VacancyModel.findOne({ + externalVacancyId, + }) + + return vacancy + } catch (error) { + console.error("DB - Erro ao buscar vaga externa por id: ", error) + throw new Error(error) + } +} diff --git a/src/routes/comment.route.js b/src/routes/comment.route.js new file mode 100644 index 0000000..1e1dc59 --- /dev/null +++ b/src/routes/comment.route.js @@ -0,0 +1,34 @@ +import express from "express" + +import * as commentController from "../controllers/comment.controller.js" + +// Middlewares +import { requiredParams } from "../middlewares/requiredParams.js" + +const router = express.Router() + +router.get("/", (req, res) => { + res.json({ message: "Publicações" }) +}) +router.post( + "/add-comment", + requiredParams(["postId", "content"]), + commentController.newComment, +) +router.post( + "/get-comments", + requiredParams(["postId"]), + commentController.getCommentsFromPost, +) +router.post( + "/remove-comment", + requiredParams(["postId", "commentId"]), + commentController.removeComment, +) +router.put( + "/edit-comment", + requiredParams(["commentId", "content"]), + commentController.editComment, +) + +export default router diff --git a/src/routes/community.route.js b/src/routes/community.route.js new file mode 100644 index 0000000..61a58ae --- /dev/null +++ b/src/routes/community.route.js @@ -0,0 +1,38 @@ +import { Router } from "express" + +// Controllers +import * as communityController from "../controllers/community.controller.js" + +// Middlewares +import { requiredParams } from "../middlewares/requiredParams.js" + +const router = Router() + +router.post( + "/create", + requiredParams(["name", "link", "platform", "professionalArea"]), + communityController.createCommunity, +) +router.post( + "/edit", + requiredParams(["name", "link", "platform", "professionalArea"]), + communityController.editCommunity, +) +router.post( + "/list", + requiredParams(["page", "filters"]), + communityController.listCommunities, +) +router.post( + "/list-by-author", + requiredParams(["page", "filters"]), + communityController.listCommunitiesByAuthor, +) +router.post( + "/rate", + requiredParams(["rating", "communityId"]), + communityController.rateCommunity, +) +router.delete("/delete/:communityId", communityController.deleteCommunity) + +export default router diff --git a/src/routes/conversation.route.js b/src/routes/conversation.route.js new file mode 100644 index 0000000..72eb16d --- /dev/null +++ b/src/routes/conversation.route.js @@ -0,0 +1,16 @@ +import { Router } from "express" + +import * as conversationController from "../controllers/conversation.controller.js" + +const router = Router() + +router.get( + "/getConversations/:keyword?", + conversationController.listConversations, +) +router.get( + "/getMessages/:conversationId/:page/:limit", + conversationController.getConversationMessages, +) + +export default router diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..a98a91e --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,36 @@ +import express from "express" + +// Middlewares +import { verifyAccessToken } from "../middlewares/verifyAccesstoken.js" + +// Routes +import userRoutes from "./user.route.js" +import postRoutes from "./posts.route.js" +import likeRoutes from "./like.route.js" +import commentRoutes from "./comment.route.js" +import professionalAreaRoutes from "./professionalArea.route.js" +import skillRoutes from "./skill.route.js" +import vacancyExternalRoutes from "./vacancyExternal.route.js" +import vacancyRoutes from "./vacancy.route.js" +import communityRoutes from "./community.route.js" +import conversationRoutes from "./conversation.route.js" +import messageRoutes from "./message.route.js" + +const router = express.Router() + +router.get("/", (req, res) => { + res.json({ message: "Bem-vindo(a)" }) +}) +router.use("/users", userRoutes) +router.use("/vacancy-external", vacancyExternalRoutes) +router.use("/posts", verifyAccessToken, postRoutes) +router.use("/likes", verifyAccessToken, likeRoutes) +router.use("/comments", verifyAccessToken, commentRoutes) +router.use("/professional-area", verifyAccessToken, professionalAreaRoutes) +router.use("/skill", verifyAccessToken, skillRoutes) +router.use("/vacancy", verifyAccessToken, vacancyRoutes) +router.use("/community", verifyAccessToken, communityRoutes) +router.use("/conversation", verifyAccessToken, conversationRoutes) +router.use("/message", verifyAccessToken, messageRoutes) + +export default router diff --git a/src/routes/like.route.js b/src/routes/like.route.js new file mode 100644 index 0000000..4a4bcd5 --- /dev/null +++ b/src/routes/like.route.js @@ -0,0 +1,20 @@ +import express from "express" + +import * as likeController from "../controllers/like.controller.js" + +// Middlewares +import { requiredParams } from "../middlewares/requiredParams.js" + +const router = express.Router() + +router.get("/", (req, res) => { + res.json({ message: "Publicações" }) +}) +router.post("/add-like", requiredParams(["postId"]), likeController.likePost) +router.post( + "/remove-like", + requiredParams(["postId"]), + likeController.unlikePost, +) + +export default router diff --git a/src/routes/message.route.js b/src/routes/message.route.js new file mode 100644 index 0000000..8461430 --- /dev/null +++ b/src/routes/message.route.js @@ -0,0 +1,17 @@ +import { Router } from "express" + +// Controllers +import * as messageController from "../controllers/message.controller.js" + +// Middlewares +import { requiredParams } from "../middlewares/requiredParams.js" + +const router = Router() + +router.post( + "/sendMessage", + requiredParams(["content", "receiverId"]), + messageController.sendMessage, +) + +export default router diff --git a/src/routes/posts.route.js b/src/routes/posts.route.js new file mode 100644 index 0000000..655fa74 --- /dev/null +++ b/src/routes/posts.route.js @@ -0,0 +1,30 @@ +import express from "express" + +import * as postsController from "../controllers/posts.controller.js" + +// Middlewares +import { requiredParams } from "../middlewares/requiredParams.js" +import { upload } from "../middlewares/upload.js" + +const router = express.Router() + +router.get("/", (req, res) => { + res.json({ message: "Publicações" }) +}) +router.post( + "/create", + upload.single("file"), + requiredParams(["content"]), + postsController.createPost, +) +router.put( + "/edit", + upload.single("file"), + requiredParams(["postId", "content"]), + postsController.editPost, +) +router.get("/get-feed", postsController.getFeed) +router.get("/get-post", postsController.getPost) +router.delete("/remove-post", postsController.deletePost) + +export default router diff --git a/src/routes/professionalArea.route.js b/src/routes/professionalArea.route.js new file mode 100644 index 0000000..38f5f91 --- /dev/null +++ b/src/routes/professionalArea.route.js @@ -0,0 +1,18 @@ +import express from "express" + +// Controllers +import * as professionalAreaController from "../controllers/professionalArea.controller.js" + +// Midlewares +import { requiredParams } from "../middlewares/requiredParams.js" + +const router = express.Router() + +router.post( + "/create", + requiredParams(["name"]), + professionalAreaController.addProfessionalArea, +) +router.get("/list", professionalAreaController.listProfessionalAreas) + +export default router diff --git a/src/routes/skill.route.js b/src/routes/skill.route.js new file mode 100644 index 0000000..782d9e8 --- /dev/null +++ b/src/routes/skill.route.js @@ -0,0 +1,26 @@ +import express from "express" + +// Controllers +import * as skillController from "../controllers/skill.controller.js" + +// Midlewares +import { requiredParams } from "../middlewares/requiredParams.js" + +const router = express.Router() + +router.post( + "/add", + requiredParams(["skillName", "professionalAreaId"]), + skillController.addSkill, +) +router.post( + "/create-many", + requiredParams(["skills", "professionalAreaId"]), + skillController.addManySkills, +) +router.get( + "/get-by-professional-area/:professionalAreaId", + skillController.getSkillByProfessinalArea, +) + +export default router diff --git a/src/routes/user.route.js b/src/routes/user.route.js new file mode 100644 index 0000000..59ea28e --- /dev/null +++ b/src/routes/user.route.js @@ -0,0 +1,193 @@ +import express from "express" + +// Controller +import * as userController from "../controllers/user.controller.js" + +// Middlewares +import { requiredParams } from "../middlewares/requiredParams.js" +import { verifyAccessToken } from "../middlewares/verifyAccesstoken.js" +import { upload } from "../middlewares/upload.js" + +const router = express.Router() + +router.get("/", userController.getAllUsers) +router.post( + "/sign-up-person", + requiredParams(["name", "email", "password"]), + userController.signUpPerson, +) +router.post( + "/sign-up-company", + requiredParams([ + "name", + "email", + "password", + "city", + "stateUf", + "occupationArea", + "companyType", + ]), + userController.signUpCompany, +) +router.post( + "/sign-in", + requiredParams(["email", "password"]), + userController.signIn, +) +router.get("/sign-out", userController.signOut) +router.get("/refresh-token", userController.refreshAccessToken) +router.post( + "/resend-confirmation-email", + userController.resendConfirmationEmail, +) +router.post("/confirm-email", userController.confirmEmail) +router.post("/forgot-password", userController.forgotPassword) +router.post("/reset-password", userController.resetPassword) +router.patch( + "/update-about", + verifyAccessToken, + requiredParams(["about"]), + userController.updateAbout, +) +router.get("/get-user", verifyAccessToken, userController.getUser) +router.patch( + "/update-basic-info", + verifyAccessToken, + requiredParams(["name"]), + userController.updateBasicInfo, +) +router.patch( + "/update-avatar", + verifyAccessToken, + upload.single("file"), + userController.updateAvatar, +) +router.post( + "/create-experience", + verifyAccessToken, + requiredParams([ + "occupation", + "company", + "startDateMonth", + "startDateYear", + "type", + ]), + userController.addExperience, +) +router.patch( + "/edit-experience", + verifyAccessToken, + requiredParams([ + "experienceId", + "occupation", + "company", + "startDateMonth", + "startDateYear", + "type", + ]), + userController.editExperience, +) +router.delete( + "/delete-experience", + verifyAccessToken, + userController.deleteExperience, +) +router.post( + "/create-academic-education", + verifyAccessToken, + requiredParams([ + "name", + "institution", + "degree", + "startDateMonth", + "startDateYear", + "endDateMonth", + "endDateYear", + ]), + userController.addAcademicEducation, +) +router.patch( + "/edit-academic-education", + verifyAccessToken, + requiredParams([ + "academicEducationId", + "name", + "institution", + "degree", + "startDateMonth", + "startDateYear", + "endDateMonth", + "endDateYear", + ]), + userController.editAcademicEducation, +) +router.delete( + "/delete-academic-education", + verifyAccessToken, + userController.deleteAcademicEducation, +) +router.post( + "/create-certificate", + verifyAccessToken, + requiredParams(["name", "institution", "issueMonth", "issueYear", "url"]), + userController.addCertificate, +) +router.patch( + "/edit-certificate", + verifyAccessToken, + requiredParams([ + "certificateId", + "name", + "institution", + "issueMonth", + "issueYear", + "url", + ]), + userController.editCertificate, +) +router.delete( + "/delete-certificate", + verifyAccessToken, + userController.deleteCertificate, +) +router.patch( + "/update-resume", + verifyAccessToken, + upload.single("file"), + userController.updateResume, +) +router.get("/get-suggestions", verifyAccessToken, userController.getSuggestions) +router.post( + "/getNetworkUsers", + verifyAccessToken, + requiredParams(["page", "limit"]), + userController.getNetworkUsers, +) +router.get( + "/get-network-user-info", + verifyAccessToken, + userController.getNetworkingUserInfo, +) +router.post("/follow", verifyAccessToken, userController.addFollower) +router.post("/unfollow", verifyAccessToken, userController.removeFollower) +router.post( + "/sendCodeToEmail", + verifyAccessToken, + requiredParams(["currentEmail", "newEmail"]), + userController.sendCodeToEmail, +) +router.post( + "/updateEmail", + verifyAccessToken, + requiredParams(["currentEmail", "newEmail", "code"]), + userController.changeEmail, +) +router.post( + "/updatePassword", + verifyAccessToken, + requiredParams(["currentPassword", "newPassword"]), + userController.changePassword, +) +router.delete("/deleteAccount", verifyAccessToken, userController.removeAccount) + +export default router diff --git a/src/routes/vacancy.route.js b/src/routes/vacancy.route.js new file mode 100644 index 0000000..daca185 --- /dev/null +++ b/src/routes/vacancy.route.js @@ -0,0 +1,72 @@ +import { Router } from "express" + +// Controllers +import * as vacancyController from "../controllers/vacancy.controller.js" + +// Middlewares +import { requiredParams } from "../middlewares/requiredParams.js" + +const router = Router() + +router.post( + "/create", + requiredParams([ + "occupation", + "company", + "description", + "typeLocation", + "occupationArea", + "skills", + "employmentType", + "contractType", + ]), + vacancyController.createVacancy, +) +router.put( + "/edit", + requiredParams([ + "vacancyId", + "occupation", + "company", + "description", + "typeLocation", + "occupationArea", + "skills", + "employmentType", + "contractType", + ]), + vacancyController.editVacancy, +) +router.post( + "/list-vacancies", + requiredParams(["page", "filters"]), + vacancyController.listVacancies, +) +router.post( + "/list-vacancies-by-author", + requiredParams(["page"]), + vacancyController.listVacanciesByAuthor, +) +router.post( + "/list-applications-by-user", + requiredParams(["page"]), + vacancyController.listApplicationsByUser, +) +router.get("/:vacancyId", vacancyController.getVacancyInfo) +router.post( + "/apply", + requiredParams(["vacancyId", "contactEmail"]), + vacancyController.applyVacancy, +) +router.post( + "/candidates", + requiredParams(["vacancyId", "page"]), + vacancyController.getVacancyCandidates, +) +router.delete("/delete/:vacancyId", vacancyController.deleteVacancy) +router.delete( + "/delete-external-vacancies", + vacancyController.removeExternalVacancies, +) + +export default router diff --git a/src/routes/vacancyExternal.route.js b/src/routes/vacancyExternal.route.js new file mode 100644 index 0000000..8ee6f42 --- /dev/null +++ b/src/routes/vacancyExternal.route.js @@ -0,0 +1,14 @@ +import { Router } from "express" + +// Controllers +import * as vacancyController from "../controllers/vacancy.controller.js" + +const router = Router() + +router.get( + "/get-external-vacancy/:id?", + vacancyController.getExternalVacancyById, +) +router.post("/create-external-vacancy", vacancyController.createExternalVacancy) + +export default router diff --git a/src/socket/socket.js b/src/socket/socket.js new file mode 100644 index 0000000..a5aa72d --- /dev/null +++ b/src/socket/socket.js @@ -0,0 +1,35 @@ +import express from "express" +import { Server } from "socket.io" +import http from "http" + +const app = express() + +const server = http.createServer(app) +const io = new Server(server, { + cors: { + origin: ["http://localhost:3000", "https://diversifind.netlify.app"], + methods: ["GET", "POST"], + }, +}) + +const userSocketMap = {} // { userId: socketId } +export function getReceiverSocketId(receiverId) { + return userSocketMap[receiverId] +} + +io.on("connection", (socket) => { + const userId = socket.handshake.query.userId + + if (userId) { + userSocketMap[userId] = socket.id + } + + io.emit("onlineUsers", Object.keys(userSocketMap)) + + socket.on("disconnect", () => { + delete userSocketMap[userId] + io.emit("onlineUsers", Object.keys(userSocketMap)) + }) +}) + +export { app, io, server } diff --git a/src/utils/createTransporter.js b/src/utils/createTransporter.js new file mode 100644 index 0000000..b532a4e --- /dev/null +++ b/src/utils/createTransporter.js @@ -0,0 +1,17 @@ +import nodemailer from "nodemailer" +import dotenv from "dotenv" + +dotenv.config() + +export function createTransporter() { + // console.log(process.env.EMAIL, process.env.EMAIL_PASSWORD) + const transporter = nodemailer.createTransport({ + service: "hotmail", + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASSWORD, + }, + }) + + return transporter +} diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..c709727 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,61 @@ +import moment from "moment" + +export function setErrorMessage(status, error, message) { + return { + status, + detail: error?.toString(), + message, + createdAt: moment(), + } +} + +export function sortExperiences(prevExperiences) { + let newExperiences = prevExperiences.sort((a, b) => { + if (b.current && !a.current) return 1 + if (a.current && !b.current) return -1 + + const endDateB = new Date(`${b.endDateYear}-${b.endDateMonth}-01`) + const endDateA = new Date(`${a.endDateYear}-${a.endDateMonth}-01`) + + if (b.endDateYear && a.endDateYear && endDateA !== endDateB) { + return endDateB - endDateA + } + + return new Date(b.createdAt) - new Date(a.createdAt) + }) + + return newExperiences +} + +export function sortFormation(prevFormations) { + let newFormations = prevFormations.sort((a, b) => { + if (b.current && !a.current) return 1 + if (a.current && !b.current) return -1 + + const endDateB = new Date(`${b.endDateYear}-${b.endDateMonth}-01`) + const endDateA = new Date(`${a.endDateYear}-${a.endDateMonth}-01`) + + if (b.endDateYear && a.endDateYear && endDateA !== endDateB) { + return endDateB - endDateA + } + + return new Date(b.createdAt) - new Date(a.createdAt) + }) + + return newFormations +} + +export function sortCertificates(prevCertificates) { + let newCertificates = prevCertificates.sort((a, b) => { + const issueDateB = new Date(`${b.issueYear}-${b.issueMonth}-01`) + const issueDateA = new Date(`${a.issueYear}-${a.issueMonth}-01`) + + if (issueDateA !== issueDateB) { + return issueDateB - issueDateA + } + + return new Date(b.createdAt) - new Date(a.createdAt) + }) + + return newCertificates +} diff --git a/src/utils/sendChangeEmailCode.js b/src/utils/sendChangeEmailCode.js new file mode 100644 index 0000000..916ef9a --- /dev/null +++ b/src/utils/sendChangeEmailCode.js @@ -0,0 +1,21 @@ +import { createTransporter } from "./createTransporter.js" +import dotenv from "dotenv" + +dotenv.config() + +export async function sendChangeEmailCode({ name, email, code }) { + const transporter = createTransporter() + + const mailOptions = { + from: process.env.EMAIL, + to: email, + subject: "Código para alteração de e-mail", + html: ` +
Este é o seu código:
+${code}
+ `, + } + + return await transporter.sendMail(mailOptions) +} diff --git a/src/utils/sendForgotPasswordEmail.js b/src/utils/sendForgotPasswordEmail.js new file mode 100644 index 0000000..f0dce4d --- /dev/null +++ b/src/utils/sendForgotPasswordEmail.js @@ -0,0 +1,25 @@ +import { createTransporter } from "./createTransporter.js" +import dotenv from "dotenv" + +dotenv.config() + +export async function sendForgotPasswordEmail(user) { + const transporter = createTransporter() + const url = + process.env.NODE_ENV === "production" + ? "https://diversifind.netlify.app" + : "http://localhost:3000" + + const mailOptions = { + from: process.env.EMAIL, + to: user.email, + subject: "Resetar senha", + html: ` +Resetar sua senha, clique no link abaixo:
+ Resetar senha + `, + } + + return await transporter.sendMail(mailOptions) +} diff --git a/src/utils/sendVerificationEmail.js b/src/utils/sendVerificationEmail.js new file mode 100644 index 0000000..8b2b92e --- /dev/null +++ b/src/utils/sendVerificationEmail.js @@ -0,0 +1,25 @@ +import { createTransporter } from "./createTransporter.js" +import dotenv from "dotenv" + +dotenv.config() + +export async function sendVerificationEmail(user) { + const transporter = createTransporter() + const url = + process.env.NODE_ENV === "production" + ? "https://diversifind.netlify.app" + : "http://localhost:3000" + + const mailOptions = { + from: process.env.EMAIL, + to: user.email, + subject: "Confirmação de email", + html: ` +Para confirmar seu email, clique no link abaixo:
+ Confirmar email + `, + } + + return await transporter.sendMail(mailOptions) +}