diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 5975b49f..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,48 +0,0 @@ -version: 2 -orbs: - coveralls: coveralls/coveralls@1.0.6 -jobs: - build: - docker: - - image: cimg/node:18.17.0 - steps: - - setup_remote_docker: - version: 20.10.7 - - checkout - - run: - name: update-npm - command: "sudo npm install -g npm@latest" - - restore_cache: - keys: - - v1-dependencies-{{ checksum "package-lock.json" }} - - v1-dependencies- - - run: - name: Install dependencies - command: npm install - - save_cache: - paths: - - node_modules - key: v1-dependencies-{{ checksum "package-lock.json" }} - - run: - name: Run tests - command: npm test - when: always - - run: - name: Run coverage - command: npm run coverage - when: always - - run: - name: Setup Code Climate test-reporter - command: | - # download test reporter as a static binary - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build - when: always - - run: - name: Send coverage report to Code Climate - command: ./cc-test-reporter after-build -t lcov - when: always - - store_artifacts: - path: ./coverage/lcov.info - prefix: tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2434151a..19fb0286 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,16 +5,19 @@ on: push: branches: - develop + env: PORT: ${{ secrets.PORT }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - DATABASE_PORT_TEST: ${{ secrets.DATABASE_PORT_TEST }} - DATABASE_HOST_TEST: ${{ secrets.DATABASE_HOST_TEST }} - DATABASE_NAME_TEST: ${{ secrets.DATABASE_NAME_TEST }} - DATABASE_USERNAME_TEST: ${{ secrets.DATABASE_USERNAME_TEST }} - DATABASE_PASSWORD_TEST: ${{ secrets.DATABASE_PASSWORD_TEST }} + DATABASE_TEST_URL: ${{ secrets.DATABASE_TEST_URL }} + NODE_EN: ${{ secrets.NODE_EN }} + DATABASE_URL_PRO: ${{ secrets.DATABASE_URL_PRO }} + API_KEY: ${{ secrets.API_KEY }} + API_SECRET: ${{ secrets.API_SECRET }} + CLOUD_NAME: ${{ secrets.CLOUD_NAME }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} jobs: build: @@ -26,20 +29,41 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} cache: "npm" - - run: npm install - - run: npm ci + + - name: Install dependencies + run: | + npm install husky --save-dev + npm install + + - name: Set NODE_ENV to test + run: echo "NODE_ENV=test" >> $GITHUB_ENV + - run: npm run build --if-present + - run: npm test --if-present + - run: npm run coverage --if-present + + - name: Setup Code Climate test-reporter + run: | + curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + chmod +x ./cc-test-reporter + ./cc-test-reporter before-build + + - name: Send coverage report to Code Climate + run: ./cc-test-reporter after-build -t lcov + - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - name: Upload coverage to Coveralls uses: coverallsapp/github-action@v2 env: diff --git a/.gitignore b/.gitignore index 6611e53d..1adebdeb 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,6 @@ node_modules/ coverage .nyc_output .vscode -dist .cache_ggshield +dist + diff --git a/.sequelizerc b/.sequelizerc index 9e2b5217..b786558b 100644 --- a/.sequelizerc +++ b/.sequelizerc @@ -1,9 +1,7 @@ -require("@babel/register"); -const path = require('path'); - +const path = require("path"); module.exports = { - "config": path.resolve('./dist/src/databases/config', 'config.js'), - "migrations-path": path.resolve('./dist/src/databases/migrations'), - "seeders-path": path.resolve('./dist/src/databases/seeders'), - "models-path": path.resolve('./dist/src/databases/models'), -}; \ No newline at end of file + 'config': path.resolve('dist/src','databases', 'config', 'config.js'), + 'models-path': path.resolve('dist/src','databases', 'models'), + 'seeders-path': path.resolve('dist/src','databases', 'seeders'), + 'migrations-path': path.resolve('dist/src','databases', 'migrations') +}; diff --git a/README.md b/README.md index 63e94bf3..c6579838 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ This is the backend for E-Commerce-Ninjas, written in Node.js with TypeScript. [![Maintainability](https://api.codeclimate.com/v1/badges/839fc3fa18d25362cd8b/maintainability)](https://codeclimate.com/github/atlp-rwanda/e-commerce-ninjas-bn/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/839fc3fa18d25362cd8b/test_coverage)](https://codeclimate.com/github/atlp-rwanda/e-commerce-ninjas-bn/test_coverage) [![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/e-commerce-ninjas-bn/badge.svg)](https://coveralls.io/github/atlp-rwanda/e-commerce-ninjas-bn) -[![CircleCI](https://dl.circleci.com/status-badge/img/gh/atlp-rwanda/e-commerce-ninjas-bn/tree/develop.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/atlp-rwanda/e-commerce-ninjas-bn/tree/develop) [![codecov](https://codecov.io/gh/atlp-rwanda/e-commerce-ninjas-bn/graph/badge.svg?token=6ZWudFPM1S)](https://codecov.io/gh/atlp-rwanda/e-commerce-ninjas-bn) ## HOSTED SERVER URL @@ -81,6 +80,40 @@ This is the backend for E-Commerce-Ninjas, written in Node.js with TypeScript. - `services/`: Service functions like sendEmails. - `index.ts`: Startup file for all requests. +## Initialize Sequelize CLI + +1. Initialize Sequelize CLI: + ```sh + npx sequelize-cli init + ``` +2. Generate Seeder: + ```sh + npx sequelize-cli seed:generate --name name-of-your-seeder + ``` +3. Generate Migrations: + ```sh + npx sequelize-cli migration:generate --name name-of-your-migration + ``` +4. Define Migration: + Edit the generated migration file to include the tables you want to create. +5. Define Seeder Data: + Edit the generated seeder file to include the data you want to insert. +6. Run the Seeder: + ```sh + npm run createAllSeeders + ``` +7. Run the Migration: + ```sh + npm run createAllTables + ``` +8. Delete the Seeder: + ```sh + npm run deleteAllSeeders + ``` +9. Delete the Migration: + ```sh + npm run deleteAllTables + ``` diff --git a/package-lock.json b/package-lock.json index 06150be6..4795edca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,23 @@ "dependencies": { "@babel/register": "^7.23.7", "@istanbuljs/nyc-config-typescript": "^1.0.2", + "bcrypt": "^5.1.1", "chai": "^4.4.1", "chai-http": "^4.4.0", + "cloudinary": "^2.2.0", "compression": "^1.7.4", "cors": "^2.8.5", "coverage": "^0.4.1", "coveralls": "^3.1.1", "dotenv": "^16.4.5", "express": "^4.19.2", + "http-status": "^1.7.4", + "joi": "^17.13.1", + "jsonwebtoken": "^9.0.2", "mocha": "^10.4.0", "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "nyc": "^15.1.0", "pg": "^8.11.5", @@ -35,14 +42,19 @@ "typescript": "^5.4.5" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@types/compression": "^1.7.5", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", "@types/mocha": "^10.0.6", "@types/morgan": "^1.9.9", + "@types/multer": "^1.4.11", "@types/node": "^20.12.12", + "@types/nodemailer": "^6.4.15", "@types/pg": "^8.11.6", "@types/sequelize": "^4.28.20", + "@types/sinon": "^17.0.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", "@typescript-eslint/eslint-plugin": "^7.10.0", @@ -51,7 +63,8 @@ "eslint": "^8.57.0", "eslint-plugin-import": "^2.29.1", "husky": "^9.0.11", - "lint-staged": "^15.2.2" + "lint-staged": "^15.2.2", + "sinon": "^18.0.0" } }, "node_modules/@ampproject/remapping": { @@ -695,6 +708,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -954,6 +980,61 @@ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" }, + "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/@mapbox/node-pre-gyp/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/@mapbox/node-pre-gyp/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/@mapbox/node-pre-gyp/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/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1003,6 +1084,68 @@ "node": ">=14" } }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1023,6 +1166,15 @@ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/bluebird": { "version": "3.5.42", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz", @@ -1144,6 +1296,15 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", @@ -1176,6 +1337,15 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, + "node_modules/@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "20.12.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", @@ -1184,6 +1354,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.15", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", + "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/pg": { "version": "8.11.6", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz", @@ -1297,6 +1476,21 @@ "@types/send": "*" } }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "node_modules/@types/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", @@ -1666,6 +1860,38 @@ "node": ">=0.4.0" } }, + "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/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -1744,6 +1970,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/append-transform": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", @@ -1755,11 +1986,29 @@ "node": ">=8" } }, + "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/archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1989,6 +2238,19 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "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/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -2092,11 +2354,27 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "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", @@ -2406,6 +2684,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -2533,6 +2819,18 @@ "node": ">=6" } }, + "node_modules/cloudinary": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.2.0.tgz", + "integrity": "sha512-akbLTZcNegGSkl07Frnt9fyiK9KZ2zPS+a+j7uLrjNYxVhDpDdIBz9G6snPCYqgk+WLVMRPfXTObalLr5L6g0Q==", + "dependencies": { + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=9" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2546,6 +2844,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "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/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -2630,6 +2936,52 @@ "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/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "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/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -2639,6 +2991,11 @@ "proto-list": "~1.2.1" } }, + "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", @@ -2973,6 +3330,11 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2990,6 +3352,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -3077,6 +3447,14 @@ "safer-buffer": "^2.1.0" } }, + "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/editorconfig": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", @@ -4206,6 +4584,33 @@ "node": ">=10" } }, + "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-minipass/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/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4268,6 +4673,71 @@ "is-windows": "^1.0.2" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "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/gauge/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/gauge/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/gauge/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/gauge/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/gauge/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/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4549,6 +5019,11 @@ "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/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -4630,6 +5105,47 @@ "npm": ">=1.3.7" } }, + "node_modules/http-status": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.7.4.tgz", + "integrity": "sha512-c2qSwNtTlHVYAhMj9JpGdyo0No/+DiKXCJ9pHtZ2Yf3QmPnBIytKSRT7BuyIiQ7icXLynavGmxUqkOjSrAuMuA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "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/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -5326,6 +5842,18 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/joi": { + "version": "17.13.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.1.tgz", + "integrity": "sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg==", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-beautify": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", @@ -5531,6 +6059,32 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/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/jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -5545,6 +6099,31 @@ "node": ">=0.6.0" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "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/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5816,11 +6395,41 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "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.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5832,6 +6441,11 @@ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" }, + "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/log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", @@ -6167,6 +6781,34 @@ "node": ">=16 || 14 >=14.17" } }, + "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/minizlib/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/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -6572,6 +7214,34 @@ "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/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6591,6 +7261,49 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, + "node_modules/nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, + "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-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -6607,6 +7320,14 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, + "node_modules/nodemailer": { + "version": "6.9.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", + "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", @@ -6723,6 +7444,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "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/nyc": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", @@ -7572,6 +8305,11 @@ "node": ">= 0.8.0" } }, + "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/process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -7623,6 +8361,15 @@ "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", @@ -7754,6 +8501,19 @@ "node": ">=4" } }, + "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/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -8487,6 +9247,54 @@ "node": ">=10" } }, + "node_modules/sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/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/sinon/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/sinon/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/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -8697,6 +9505,22 @@ "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-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -9013,6 +9837,35 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "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/tar/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/tar/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/test-exclude": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", @@ -9097,6 +9950,11 @@ "node": ">=0.8" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -9380,6 +10238,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -9494,6 +10357,11 @@ "punycode": "^2.1.0" } }, + "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", @@ -9567,6 +10435,20 @@ "extsprintf": "^1.2.0" } }, + "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/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/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -9618,6 +10500,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/wkx": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", diff --git a/package.json b/package.json index 90969858..c5857169 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ "deleteAllSeeders": "tsc && npx sequelize db:seed:undo:all", "start": "ts-node-dev src/index.ts", "dev": "ts-node-dev src/index.ts", - "test": "cross-env NODE_ENV=test npm run deleteAllTables && cross-env NODE_ENV=test npm run createAllTables && cross-env NODE_ENV=test npm run createAllSeeders && nyc mocha --require ts-node/register './src/**/*.spec.ts' --timeout 300000 --exit", + "test": "cross-env NODE_ENV=test npm run deleteAllTables && cross-env NODE_ENV=test npm run createAllTables && cross-env NODE_ENV=test npm run createAllSeeders && nyc cross-env NODE_ENV=test mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 600000 --exit", "coveralls": "nyc --reporter=lcov --reporter=text-lcov npm test | coveralls", - "coverage": "nyc mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 300000 --exit", + "coverage": " nyc mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 600000 --exit", "lint": "eslint . --ext .ts", "lint-staged": "lint-staged", "prepare": "husky" @@ -52,16 +52,23 @@ "dependencies": { "@babel/register": "^7.23.7", "@istanbuljs/nyc-config-typescript": "^1.0.2", + "bcrypt": "^5.1.1", "chai": "^4.4.1", "chai-http": "^4.4.0", + "cloudinary": "^2.2.0", "compression": "^1.7.4", "cors": "^2.8.5", "coverage": "^0.4.1", "coveralls": "^3.1.1", "dotenv": "^16.4.5", "express": "^4.19.2", + "http-status": "^1.7.4", + "joi": "^17.13.1", + "jsonwebtoken": "^9.0.2", "mocha": "^10.4.0", "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "nyc": "^15.1.0", "pg": "^8.11.5", @@ -76,14 +83,19 @@ "typescript": "^5.4.5" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@types/compression": "^1.7.5", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", "@types/mocha": "^10.0.6", "@types/morgan": "^1.9.9", + "@types/multer": "^1.4.11", "@types/node": "^20.12.12", + "@types/nodemailer": "^6.4.15", "@types/pg": "^8.11.6", "@types/sequelize": "^4.28.20", + "@types/sinon": "^17.0.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", "@typescript-eslint/eslint-plugin": "^7.10.0", @@ -92,6 +104,7 @@ "eslint": "^8.57.0", "eslint-plugin-import": "^2.29.1", "husky": "^9.0.11", - "lint-staged": "^15.2.2" + "lint-staged": "^15.2.2", + "sinon": "^18.0.0" } } diff --git a/public/BUILD.txt b/public/BUILD.txt new file mode 100644 index 00000000..0099cd3b --- /dev/null +++ b/public/BUILD.txt @@ -0,0 +1,11 @@ +Believe: Know that the problem you have identified has a solution, and trust that you and your team can build something to address it. + +Understand: Take the time to learn about the experience of the people affected by this problem / who will be using your solution – what does their journey look like? What are their pain points? What factors are affecting how they operate in / engage with the world? + +Invent: Create a Minimum Viable Product or prototype to test out an idea that you have! Be sure to keep in mind how your “invention” will meet your users’ needs. + +Listen: Get feedback from your users on your MVP / prototype and modify it as needed. + +Deliver: Continue to deliver refined versions of your MVP / prototype and improve it over time! + +Password@123 \ No newline at end of file diff --git a/public/ProjectManagement.jpg b/public/ProjectManagement.jpg new file mode 100644 index 00000000..1f26cd38 Binary files /dev/null and b/public/ProjectManagement.jpg differ diff --git a/src/databases/config/config.ts b/src/databases/config/config.ts index 308540cd..de78e28f 100644 --- a/src/databases/config/config.ts +++ b/src/databases/config/config.ts @@ -1,43 +1,30 @@ import dotenv from "dotenv" -dotenv.config() -module.exports = { - test: { - logging: false, - dialect: "postgres", - port: process.env.DATABASE_PORT_TEST, - host: process.env.DATABASE_HOST_TEST, - database: process.env.DATABASE_NAME_TEST, - username: process.env.DATABASE_USERNAME_TEST, - password: process.env.DATABASE_PASSWORD_TEST, - dialectOptions: { - ssl: { - require: true, - rejectUnauthorized: true - } - } +dotenv.config() - }, +const commonDatabaseConfig = { + dialect: "postgres", + dialectOptions: { + ssl: { + require: true, + rejectUnauthorized: false + } + } +} - development: { - logging: false, - dialect: "postgres", - port: process.env.DATABASE_PORT_DEV, - host: process.env.DATABASE_HOST_DEV, - database: process.env.DATABASE_NAME_DEV, - username: process.env.DATABASE_USERNAME_DEV, - password: process.env.DATABASE_PASSWORD_DEV - }, +const sequelizeConfig = { + development: { + ...commonDatabaseConfig, + url: process.env.DEV_DATABASE_URL + }, + test: { + ...commonDatabaseConfig, + url: process.env.DATABASE_TEST_URL + }, + production: { + ...commonDatabaseConfig, + url: process.env.DATABASE_URL_PRO + } +} - production: { - logging: false, - dialect: "postgres", - url: process.env.DATABASE_URL_PRO, - dialectOptions: { - ssl: { - require: true, - rejectUnauthorized: true - } - } - } -} \ No newline at end of file +module.exports = sequelizeConfig \ No newline at end of file diff --git a/src/databases/config/db.config.ts b/src/databases/config/db.config.ts new file mode 100644 index 00000000..2bf64a77 --- /dev/null +++ b/src/databases/config/db.config.ts @@ -0,0 +1,44 @@ +import { config } from "dotenv" +import { Sequelize } from "sequelize" + +config() +const NODE_ENV: string = process.env.NODE_ENV || "development" +const DB_HOST_MODE: string = process.env.DB_HOST_TYPE || "remote" + +/** + * Get the URI for the database connection. + * @returns {string} The URI string. + */ +function getDbUri(): string { + switch (NODE_ENV) { + case "test": + return process.env.DATABASE_TEST_URL as string + case "production": + return process.env.DATABASE_URL_PRO as string + default: + return process.env.DEV_DATABASE_URL as string + } +} + +/** + * Get dialect options for Sequelize. + * @returns {DialectOptions} The dialect options. + */ +function getDialectOptions() { + return DB_HOST_MODE === "local" + ? {} + : { + ssl: { + require: true, + rejectUnauthorized: false + } + } +} + +const sequelizeConnection: Sequelize = new Sequelize(getDbUri(), { + dialect: "postgres", + dialectOptions: getDialectOptions(), + logging: false +}) + +export default sequelizeConnection diff --git a/src/databases/migrations/20240520180022-create-users.ts b/src/databases/migrations/20240520180022-create-users.ts index e475afd6..7fdecf23 100644 --- a/src/databases/migrations/20240520180022-create-users.ts +++ b/src/databases/migrations/20240520180022-create-users.ts @@ -1,6 +1,16 @@ import { QueryInterface, DataTypes } from "sequelize"; + export default { up: async (queryInterface: QueryInterface) => { + const [results] = await queryInterface.sequelize.query( + "SELECT 1 FROM pg_type WHERE typname = 'enum_users_gender';" + ); + if (!results.length) { + await queryInterface.sequelize.query( + "CREATE TYPE \"enum_users_gender\" AS ENUM('male', 'female');" + ); + } + await queryInterface.createTable("users", { id: { type: DataTypes.INTEGER, @@ -28,6 +38,26 @@ export default { type: new DataTypes.BIGINT, allowNull: false }, + profilePicture: { + type: new DataTypes.STRING(128), + allowNull: false + }, + gender: { + type: new DataTypes.ENUM("male", "female"), + allowNull: false + }, + birthDate: { + type: new DataTypes.DATE, + allowNull: false + }, + language: { + type: new DataTypes.STRING(128), + allowNull: false + }, + currency: { + type: new DataTypes.STRING(128), + allowNull: false + }, role: { type: new DataTypes.STRING(128), allowNull: false @@ -37,25 +67,36 @@ export default { allowNull: false, defaultValue: false }, - status: { + is2FAEnabled: { type: new DataTypes.BOOLEAN, allowNull: false, defaultValue: false }, + status: { + type: new DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true + }, createdAt: { allowNull: false, type: DataTypes.DATE, - defaultValue: DataTypes.DATE + defaultValue: DataTypes.NOW }, updatedAt: { allowNull: false, type: DataTypes.DATE, - defaultValue: DataTypes.DATE + defaultValue: DataTypes.NOW } }); }, down: async (queryInterface: QueryInterface) => { + // Drop the users table await queryInterface.dropTable("users"); + + // Drop the enum type if it exists + await queryInterface.sequelize.query(` + DROP TYPE IF EXISTS "enum_users_gender"; + `); } }; diff --git a/src/databases/models/index.ts b/src/databases/models/index.ts deleted file mode 100644 index 0824e9cf..00000000 --- a/src/databases/models/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-var-requires */ -import fs from "fs"; -import path from "path"; -import { Sequelize, DataTypes } from "sequelize"; -import * as dbConnection from "../config/config" - -const db: any = {}; -let sequelize: Sequelize; -const basename = path.basename(__filename); -const env: string = process.env.NODE_ENV || "development"; -const config: any = dbConnection[env as keyof typeof dbConnection]; - -if (config.url) { - sequelize = new Sequelize(config.url, config); -} else { - sequelize = new Sequelize(config.database!, config.username!, config.password, config); -} - -fs.readdirSync(__dirname) - .filter((file: string) => { - return file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".ts"; - }) - .forEach((file: string) => { - const modelPath = path.join(__dirname, file); - const model = require(modelPath)(sequelize, DataTypes); - db[model.name] = model; - }); - -Object.keys(db).forEach((modelName) => { - if (db[modelName].associate) { - db[modelName].associate(db); - } -}); - -db.sequelize = sequelize; -db.Sequelize = Sequelize; - -export default db; diff --git a/src/databases/models/users.ts b/src/databases/models/users.ts index 91affedd..ee2d02f1 100644 --- a/src/databases/models/users.ts +++ b/src/databases/models/users.ts @@ -1,36 +1,49 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable require-jsdoc */ -import { Model, DataTypes, Sequelize } from "sequelize"; +import { Model, DataTypes } from "sequelize"; +import sequelizeConnection from "../config/db.config" +import bcrypt from "bcrypt"; -interface UsersAttributes { +export interface UsersAttributes { id: number; firstName: string; lastName: string; email: string; password: string; - phone:number; + phone: number; + profilePicture: string; + gender: string; + birthDate: string; + language: string; + currency: string; role: string; isVerified: boolean; + is2FAEnabled: boolean; status: boolean; createdAt: Date; updatedAt: Date; } -module.exports = (sequelize: Sequelize) => { class Users extends Model implements UsersAttributes { declare id: number; declare firstName: string; declare lastName: string; declare email: string; - declare phone:number; + declare phone: number; + declare profilePicture: string; + declare gender: string; + declare birthDate: string; + declare language: string; + declare currency: string; declare role: string; declare isVerified: boolean; + declare is2FAEnabled: boolean; declare status: boolean; declare password: string; declare createdAt: Date; declare updatedAt: Date; // Define any static methods or associations here - } Users.init( @@ -54,12 +67,34 @@ module.exports = (sequelize: Sequelize) => { }, password: { type: new DataTypes.STRING(128), - allowNull: false + allowNull: false, + defaultValue: "url" }, phone: { type: new DataTypes.BIGINT, allowNull: false }, + profilePicture: { + type: new DataTypes.STRING(128), + allowNull: false, + defaultValue: "https://upload.wikimedia.org/wikipedia/commons/5/59/User-avatar.svg" + }, + gender: { + type: new DataTypes.ENUM("male", "female"), + allowNull: false + }, + birthDate: { + type: new DataTypes.DATEONLY, + allowNull: false + }, + language: { + type: new DataTypes.STRING(128), + allowNull: false + }, + currency: { + type: new DataTypes.STRING(128), + allowNull: false + }, role: { type: new DataTypes.STRING(128), allowNull: false @@ -69,11 +104,16 @@ module.exports = (sequelize: Sequelize) => { allowNull: false, defaultValue: false }, - status: { + is2FAEnabled: { type: new DataTypes.BOOLEAN, allowNull: false, defaultValue: false }, + status: { + type: new DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true + }, createdAt: { field: "createdAt", type: DataTypes.DATE, @@ -88,12 +128,16 @@ module.exports = (sequelize: Sequelize) => { } }, { - sequelize, + sequelize:sequelizeConnection, tableName: "users", timestamps: true, - modelName:"Users" + modelName: "Users", + hooks: { + beforeCreate: async (user) => { + user.password = await bcrypt.hash(user.password, 10); + } + } } ); - return Users; -}; + export default Users; diff --git a/src/databases/seeders/20240520202759-users.ts b/src/databases/seeders/20240520202759-users.ts index 1ea0da17..09d8f991 100644 --- a/src/databases/seeders/20240520202759-users.ts +++ b/src/databases/seeders/20240520202759-users.ts @@ -1,5 +1,4 @@ import { QueryInterface } from "sequelize" - const userOne = { createdAt: new Date(), updatedAt: new Date(), @@ -8,16 +7,26 @@ const userOne = { email:"hyassin509@gmail.com", password:"$321!pass!123$", phone:25089767899, + profilePicture: "", + gender: "female", + birthDate: "2-2-2014", + language: "english", + currency: "USD", role: "buyer" } const userTwo = { createdAt: new Date(), updatedAt: new Date(), - firstName:"aime509", - lastName:"aime209", - email:"aime509@gmail.com", - password:"$321!pass!123$", - phone:25089767899, + firstName: "John", + lastName: "Doe", + email: "john.doe@example.com", + password: "password123", + phone: 1234567890, + profilePicture: "http://example.com/profile.jpg", + gender: "male", + birthDate: "1990-01-01", + language: "English", + currency: "USD", role: "buyer" } @@ -29,6 +38,11 @@ const userThree = { email:"paccy509@gmail.com", password:"$321!pass!123$", phone:25089767899, + profilePicture: "", + gender: "female", + birthDate: "2-2-2014", + language: "english", + currency: "USD", role: "buyer" } diff --git a/src/helpers/index.ts b/src/helpers/index.ts index defa0abe..05d1aaf4 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1 +1,9 @@ -// Helpers \ No newline at end of file +import jwt from "jsonwebtoken" +import dotenv from "dotenv" + +dotenv.config + + const generateToken = (id: number) => { + return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "1h" }); + }; + export { generateToken} \ No newline at end of file diff --git a/src/helpers/multer.ts b/src/helpers/multer.ts new file mode 100644 index 00000000..826fa6a5 --- /dev/null +++ b/src/helpers/multer.ts @@ -0,0 +1,14 @@ +import { Request } from "express"; +import multer from "multer"; +import path from "path"; + +export default multer({ + storage:multer.diskStorage({}), + fileFilter:(req:Request,file:Express.Multer.File,cb)=>{ + const ext = path.extname(file.originalname); + if(ext!== ".png" && ext!== ".jpg" && ext!== ".jpeg"){ + return cb(new Error("Only images are allowed")); + } + cb(null,true); + } +}) \ No newline at end of file diff --git a/src/helpers/uploadImage.ts b/src/helpers/uploadImage.ts new file mode 100644 index 00000000..d63abf58 --- /dev/null +++ b/src/helpers/uploadImage.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { v2 as cloudinary } from "cloudinary"; + +cloudinary.config({ + cloud_name: process.env.CLOUD_NAME, + api_key: process.env.API_KEY, + api_secret: process.env.API_SECRET + }); + +export const uploadImages = async (fileToUpload: any): Promise<{ public_id: string; secure_url: string }> => { + const result = await cloudinary.uploader.upload(fileToUpload.path); + return { + public_id: result.public_id, + secure_url: result.secure_url + }; + }; + +export default uploadImages; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 58c78292..fbd6c8ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import compression from "compression"; import cors from "cors"; import SwaggerUi from "swagger-ui-express"; import Document from "../swagger.json"; +import router from "./routes"; dotenv.config(); const app: Express = express(); @@ -12,9 +13,10 @@ const app: Express = express(); const PORT = process.env.PORT; app.use(express.json()); -app.use(morgan(process.env.NODE)); +app.use(morgan(process.env.NODE_EN)); app.use(compression()); app.use(cors()); +app.use("/api",router); app.use("/api-docs", SwaggerUi.serve, SwaggerUi.setup(Document)); app.get("/", (req: Request, res: Response) => { diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index b120e45b..0de523ef 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -1 +1,59 @@ -// Middewares \ No newline at end of file +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { NextFunction, Request, Response } from "express"; +import jwt from "jsonwebtoken"; +import Users from "../databases/models/users"; + +interface UserInterface { + createdAt: Date; + updatedAt: Date; + firstName: string; + lastName: string; + email: string; + password: string; + phone: number; + profilePicture: string; + gender: string; + birthDate: string; + language: string; + currency: string; + role: string; +} + +interface ExtendedRequest extends Request { + user: UserInterface; +} + +export const protect = async function ( + req: ExtendedRequest, + res: Response, + next: NextFunction +) { + try { + //? 1. Get token and check if it's there + let token: string; + if (req.headers.authorization?.startsWith("Bearer")) { + token = req.headers.authorization.split(" ").at(-1); + } + + if (!token) throw new Error("Login to get access to this resource"); + + //? 2. Validate the token to see if it is correct or if it has not expired + const decoded = await jwt.verify(token, process.env.JWT_SECRET); + + //? 3. Check if the user still exists + const curUser = await Users.findByPk((decoded as any).id); + if (!curUser) { + throw new Error("User belonging to this token does not exist"); + } + + //? Grant access to the protected route + req.user = curUser; + next(); + } catch (err: any) { + let message: string; + if (err.name === "JsonWebTokenError" || err.name === "TokenExpiredError") { + message = "Invalid JWT token. Log in again to get a new one"; + } else message = err.message; + res.status(401).json({ ok: false, status: "fail", message: message }); + } +}; diff --git a/src/middlewares/validation.ts b/src/middlewares/validation.ts new file mode 100644 index 00000000..c3cdd8dc --- /dev/null +++ b/src/middlewares/validation.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { NextFunction, Request, Response } from "express"; +import authRepositories from "../modules/auth/repository/authRepositories"; +import { UsersAttributes } from "../databases/models/users"; +import Joi from "joi"; +import httpStatus from "http-status"; + +const validation = (schema: Joi.ObjectSchema | Joi.ArraySchema) => async (req: Request, res: Response, next: NextFunction) => { + try { + const { error } = schema.validate(req.body, { abortEarly: false }); + + if (error) { + throw new Error(error.details.map((detail) => detail.message.replace(/"/g, "")).join(", ")); + } + + return next(); + } catch (error) { + res.status(httpStatus.BAD_REQUEST).json({ status: httpStatus.BAD_REQUEST, message: error.message }); + } +}; + + +const isUserExist = async (req: Request, res: Response, next: NextFunction) => { + try { + const email:string = req.body.email + const userExists:UsersAttributes = await authRepositories.findUserByEmail(email); + if (userExists) { + return res.status(httpStatus.BAD_REQUEST).json({ status: httpStatus.BAD_REQUEST, message: "User already exists." }); + } + return next(); + } catch (error) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR , message: error.message}) + } + +} + +export {validation,isUserExist}; \ No newline at end of file diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts new file mode 100644 index 00000000..183f4492 --- /dev/null +++ b/src/modules/auth/controller/authControllers.ts @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Request, Response } from "express"; +import userRepositories from "../repository/authRepositories"; +import { generateToken } from "../../../helpers"; +import httpStatus from "http-status"; +import { UsersAttributes } from "../../../databases/models/users"; +import uploadImages from "../../../helpers/uploadImage"; + +const registerUser = async (req: Request, res: Response): Promise => { + try { + const result = await uploadImages(req.file); + req.body.profilePicture = result.secure_url; + const register:UsersAttributes = await userRepositories.registerUser(req.body); + const token: string = generateToken(register.id); + res.status(httpStatus.OK).json({ user: register, token: token }) + }catch(error) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); + } + +} + +export default { registerUser } \ No newline at end of file diff --git a/src/modules/auth/repository/authRepositories.ts b/src/modules/auth/repository/authRepositories.ts new file mode 100644 index 00000000..cb8e6b9b --- /dev/null +++ b/src/modules/auth/repository/authRepositories.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import Users from "../../../databases/models/users" + +const registerUser = async (body:any) =>{ + return await Users.create(body) +} + +const findUserByEmail = async (email:string) =>{ + return await Users.findOne({ where: { email: email} }) +} + + +export default {registerUser, findUserByEmail} \ No newline at end of file diff --git a/src/modules/auth/test/auth.spec.ts b/src/modules/auth/test/auth.spec.ts new file mode 100644 index 00000000..b8343bf4 --- /dev/null +++ b/src/modules/auth/test/auth.spec.ts @@ -0,0 +1,210 @@ +import { Request, Response } from "express"; +import chai, { expect } from "chai"; +import chaiHttp from "chai-http"; +import sinon from "sinon"; +import httpStatus from "http-status"; +import fs from "fs" +import path from "path"; +import app from "../../.."; +import { isUserExist } from "../../../middlewares/validation"; +import authRepositories from "../repository/authRepositories"; +import Users from "../../../databases/models/users"; +import * as uploadImages from "../../../helpers/uploadImage"; + + + +chai.use(chaiHttp); +const router = () => chai.request(app); + +const imagePath = path.join(__dirname, "../../../../public/ProjectManagement.jpg"); +const filePath = path.join(__dirname, "../../../../public/BUILD.txt"); +const fileBuffer = fs.readFileSync(filePath); +const imageBuffer = fs.readFileSync(imagePath) +describe("Authentication Test Cases", () => { + it("Should be able to register new user", (done) => { + router() + .post("/api/auth/register") + .field("firstName", "TestUser") + .field("lastName", "TestUser") + .field("email", "TestUser@example.com") + .field("phone", 123456789) + .field("password", "TestUser@123") + .field("gender", "male") + .field("language", "english") + .field("currency", "USD") + .field("birthDate", "2000-12-12") + .field("role", "buyer") + .attach("profilePicture", imageBuffer, "ProjectManagement.jpg") + .end((error, response) => { + if (error || response.status !== 200) { + console.error("Error:", error); + console.log("Response Body:", response.body); + } + expect(response.status).to.equal(200); + expect(response.body).to.be.a("object"); + expect(response.body).to.have.property("user"); + expect(response.body).to.have.property("token"); + expect(response.body.user).to.have.property("firstName"); + expect(response.body.user).to.have.property("lastName"); + expect(response.body.user).to.have.property("email"); + expect(response.body.user).to.have.property("phone"); + expect(response.body.user).to.have.property("password"); + expect(response.body.user).to.have.property("role"); + expect(response.body.user).to.have.property("id"); + expect(response.body.user).to.have.property("gender"); + expect(response.body.user).to.have.property("language"); + expect(response.body.user).to.have.property("currency"); + expect(response.body.user).to.have.property("profilePicture"); + expect(response.body.user).to.have.property("birthDate"); + expect(response.body.user).to.have.property("status", true); + expect(response.body.user).to.have.property("isVerified", false); + expect(response.body.user).to.have.property("is2FAEnabled", false); + expect(response.body.user).to.have.property("createdAt"); + expect(response.body.user).to.have.property("updatedAt"); + done(error); + }); + }); + + it("should return validation return message error and 400", (done) => { + router() + .post("/api/auth/register") + .field("firstName", "TestUser") + .field("lastName", "TestUser") + .field("email", "TestUser@example.com") + .field("phone", "8888888") + .field("password", "TestUser") + .field("gender", "male") + .field("language", "english") + .field("currency", "USD") + .field("birthDate", "2000-12-12") + .field("role", "buyer") + .attach("profilePicture", imageBuffer, "ProjectManagement.jpg") + .end((error, response) => { + expect(response.status).equal(400); + expect(response.body).to.be.a("object"); + expect(response.body).to.have.property("message"); + done(error); + }); + }); +}) +describe("multer test", () => { + it("should return fail", (done) => { + router() + .post("/api/auth/register") + .field("firstName", "TestUser") + .field("lastName", "TestUser") + .field("email", "TestUser@example.com") + .field("phone", "8888888") + .field("password", "TestUser") + .field("gender", "male") + .field("language", "english") + .field("currency", "USD") + .field("birthDate", "2000-12-12") + .field("role", "buyer") + .attach("profilePicture", fileBuffer, "BUILD.txt") + .end((err, res) => { + expect(res.body).to.be.an("object"); + done(err); + }); + }) +}) + +describe("isUserExist Middleware", () => { + before(() => { + app.post("/auth/register", isUserExist, (req: Request, res: Response) => { + res.status(200).json({ message: "success" }); + }); + }); + + afterEach(async () => { + sinon.restore(); + await Users.destroy({ where: {} }); + }); + + it("should return user already exists", (done) => { + + router() + .post("/api/auth/register") + .field("firstName", "TestUser") + .field("lastName", "TestUser") + .field("email", "TestUser@example.com") + .field("phone", 123456789) + .field("password", "TestUser@123") + .field("gender", "male") + .field("language", "english") + .field("currency", "USD") + .field("birthDate", "2000-12-12") + .field("role", "buyer") + .attach("profilePicture", imageBuffer, "ProjectManagement.jpg") + .end((err, res) => { + expect(res).to.have.status(httpStatus.BAD_REQUEST); + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("status", httpStatus.BAD_REQUEST); + expect(res.body).to.have.property("message", "User already exists."); + done(err); + }); + }); + + it("should return internal server error", (done) => { + sinon.stub(authRepositories, "findUserByEmail").throws(new Error("Database error")); + router() + .post("/auth/register") + .field("email", "usertesting@gmail.com") + .end((err, res) => { + expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("status", httpStatus.INTERNAL_SERVER_ERROR); + expect(res.body).to.have.property("message","Database error"); + done(err); + }); + }); + + it("should call next if user does not exist", (done) => { + sinon.stub(authRepositories, "findUserByEmail").resolves(null); + + router() + .post("/auth/register") + .field("email", "newuser@gmail.com") + .end((err, res) => { + expect(res).to.have.status(200); + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("message", "success"); + done(err); + }); + }); +}); + +describe("POST /api/auth/register error handling", () => { + let uploadImagesStub: sinon.SinonStub; + + beforeEach(() => { + uploadImagesStub = sinon.stub(uploadImages, "uploadImages").throws(new Error("Invalid image file")); + }); + + afterEach(() => { + uploadImagesStub.restore(); + }); + + it("should return 500 and an error message when an error occurs", (done) => { + router() + .post("/api/auth/register") + .field("firstName", "TestUser") + .field("lastName", "TestUser") + .field("email", "TestUser@example.com") + .field("phone", 123456789) + .field("password", "TestUser@123") + .field("gender", "male") + .field("language", "english") + .field("currency", "USD") + .field("birthDate", "2000-12-12") + .field("role", "buyer") + .attach("profilePicture", Buffer.from("image content"), "ProjectManagement.jpg") + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(500); + expect(res.body).to.have.property("status", 500); + expect(res.body).to.have.property("message", "Invalid image file"); + done(); + }); + }); +}); \ No newline at end of file diff --git a/src/modules/auth/validation/authValidations.ts b/src/modules/auth/validation/authValidations.ts new file mode 100644 index 00000000..9553d3a2 --- /dev/null +++ b/src/modules/auth/validation/authValidations.ts @@ -0,0 +1,75 @@ +import Joi from "joi"; + +interface User { + firstName: string; + lastName: string; + email: string; + password: string; + phone: number; + profilePicture?: string; + gender: "male" | "female" | "other"; + birthDate: string; + language: string; + currency: string; + role: "buyer" | "seller" | "admin"; +} + +const authSchema = Joi.object({ + firstName: Joi.string().required().messages({ + "string.base": "firstName should be a type of text", + "string.empty": "firstName cannot be an empty field", + "any.required": "firstName is required" + }), + lastName: Joi.string().required().messages({ + "string.base": "lastName should be a type of text", + "string.empty": "lastName cannot be an empty field", + "any.required": "lastName is required" + }), + email: Joi.string().email().required().messages({ + "string.base": "email should be a type of text", + "string.email": "email must be a valid email", + "string.empty": "email cannot be an empty field", + "any.required": "email is required" + }), + password: Joi.string().min(8).pattern(new RegExp("^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9]).{8,}$")).required().messages({ + "string.base": "password should be a type of text", + "string.empty": "password cannot be an empty field", + "string.min": "password should have a minimum length of 8", + "string.pattern.base": "password must contain letters, numbers, and special characters", + "any.required": "password is required" + }), + phone: Joi.number().required().messages({ + "number.base": "phone number should be a type of number", + "any.required": "phone number is required" + }), + // profilePicture: Joi.string().uri().optional().messages({ + // "string.base": "profilePicture should be a type of text", + // }), + gender: Joi.string().valid("male", "female", "other").required().messages({ + "string.base": "gender should be a type of text", + "any.only": "gender must be one of [male, female, other]", + "any.required": "gender is required" + }), + birthDate: Joi.date().iso().required().messages({ + "date.base": "birthDate should be a valid date", + "date.iso": "birthDate must be in ISO format", + "any.required": "birthDate is required" + }), + language: Joi.string().required().messages({ + "string.base": "language should be a type of text", + "string.empty": "language cannot be an empty field", + "any.required": "language is required" + }), + currency: Joi.string().required().messages({ + "string.base": "currency should be a type of text", + "string.empty": "currency cannot be an empty field", + "any.required": "currency is required" + }), + role: Joi.string().valid("buyer", "seller", "admin").required().messages({ + "string.base": "role should be a type of text", + "any.only": "role must be one of [buyer, seller, admin]", + "any.required": "role is required" + }) +}); + +export {authSchema}; \ No newline at end of file diff --git a/src/modules/user/controller/userControllers.ts b/src/modules/user/controller/userControllers.ts deleted file mode 100644 index 70972d02..00000000 --- a/src/modules/user/controller/userControllers.ts +++ /dev/null @@ -1 +0,0 @@ -// user Controllers \ No newline at end of file diff --git a/src/modules/user/repository/userRepositories.ts b/src/modules/user/repository/userRepositories.ts deleted file mode 100644 index 1dd76dab..00000000 --- a/src/modules/user/repository/userRepositories.ts +++ /dev/null @@ -1 +0,0 @@ -// user repositories \ No newline at end of file diff --git a/src/modules/user/test/user.spec.ts b/src/modules/user/test/user.spec.ts deleted file mode 100644 index 1da3e264..00000000 --- a/src/modules/user/test/user.spec.ts +++ /dev/null @@ -1 +0,0 @@ -// user Tests \ No newline at end of file diff --git a/src/modules/user/validation/userValidations.ts b/src/modules/user/validation/userValidations.ts deleted file mode 100644 index 75beb045..00000000 --- a/src/modules/user/validation/userValidations.ts +++ /dev/null @@ -1 +0,0 @@ -// user validations \ No newline at end of file diff --git a/src/routes/authRouter.ts b/src/routes/authRouter.ts new file mode 100644 index 00000000..1c1c590d --- /dev/null +++ b/src/routes/authRouter.ts @@ -0,0 +1,12 @@ +import {validation,isUserExist} from "../middlewares/validation"; +import authControllers from "../modules/auth/controller/authControllers"; +import { Router } from "express"; +import {authSchema} from "../modules/auth/validation/authValidations"; +import upload from "../helpers/multer"; + +const router: Router = Router(); + +router.post("/register", upload.single("profilePicture"), validation(authSchema), isUserExist, authControllers.registerUser); + + +export default router; \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index 0532cbff..fdd654b3 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1 +1,8 @@ -// Use Routes +import { Router } from "express" +import authRouter from "./authRouter" + +const router: Router = Router() + +router.use("/auth", authRouter); + +export default router; \ No newline at end of file diff --git a/src/services/sendEmail.ts b/src/services/sendEmail.ts index 987413f4..e69de29b 100644 --- a/src/services/sendEmail.ts +++ b/src/services/sendEmail.ts @@ -1 +0,0 @@ -// Sending Email. \ No newline at end of file diff --git a/swagger.json b/swagger.json index e6bc1b4c..ba10b850 100644 --- a/swagger.json +++ b/swagger.json @@ -3,7 +3,7 @@ "info": { "version": "1.0.0", "title": "E-commerce-ninjas", - "description": "APIs for the E-commmerce-ninjas Project", + "description": "APIs for the E-commerce-ninjas Project", "termsOfService": "https://github.com/atlp-rwanda/e-commerce-ninjas-bn/blob/develop/README.md", "contact": { "email": "e-commerce-ninjas@andela.com" @@ -19,6 +19,10 @@ { "name": "Initial Route", "description": "Initial/Fake Endpoint | GET Route" + }, + { + "name": "Register Route", + "description": "Registration for user | POST Route" } ], "schemes": [ @@ -39,7 +43,7 @@ "tags": [ "Initial Route" ], - "summary": "Handle Intial / Wrong GET Route)", + "summary": "Handle Initial / Wrong GET Route", "description": "By the use of initial/wrong GET endpoint, you will be able to see welcome message (Welcome to E-Commerce-Ninja-BackEnd)", "responses": { "200": { @@ -47,6 +51,172 @@ } } } + }, + "/api/auth/register": { + "post": { + "tags": [ + "Register Route" + ], + "summary": "Register a new user", + "description": "This endpoint allows for registering a new user by providing the required details.", + "requestBody": { + "description": "User registration details", + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "in":"formData" + }, + "lastName": { + "type": "string", + "in":"formData" + }, + "email": { + "type": "string", + "format": "email", + "in":"formData" + }, + "password": { + "type": "string", + "format": "password", + "in":"formData" + }, + "phone": { + "type": "number", + "in":"formData" + }, + "profilePicture":{ + "type": "string", + "format": "binary", + "in":"formData" + }, + "gender": { + "type": "string", + "enum": ["male", "female"], + "in":"formData" + }, + "birthDate": { + "type": "string", + "format": "date" + + }, + "language": { + "type": "string", + "in":"formData" + }, + "currency": { + "type": "string", + "in":"formData" + }, + "role": { + "type": "string", + "in":"formData" + } + }, + "required": ["firstName", "lastName", "email", "password", "phone", "gender", "birthDate", "language", "currency", "role"] + } + } + } + }, + "responses": { + "200": { + "description": "User registered successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "$ref": "#/components/schemas/User" + }, + "token": { + "type": "string" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "integer" + }, + "error": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "integer" + }, + "profilePicture": { + "type": "string", + "format": "uri" + }, + "gender": { + "type": "string", + "enum": ["male", "female"] + }, + "birthDate": { + "type": "string", + "format": "date" + }, + "language": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "role": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + } } } -} \ No newline at end of file +}