diff --git a/.editorconfig b/.editorconfig index 2d78145..96cb5b7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,6 +3,9 @@ # Top-most EditorConfig file root = true +[*.{js,jsx,ts,tsx}] +quote_type = single + [*] # Set default charset to utf-8 charset = utf-8 diff --git a/.env.sample b/.env.sample index da262c4..68ff74d 100644 --- a/.env.sample +++ b/.env.sample @@ -3,6 +3,7 @@ CORE_PORT=8000 NODE_ENV=development CORS_ORIGIN_WHITELIST=http://localhost:3000,http://127.0.0.1:3000 +AUTH=false RATE_LIMIT_ENABLED=false RATE_LIMIT_WINDOW_MS=1000 @@ -10,4 +11,12 @@ RATE_LIMIT_MAX_REQUESTS=10 DB=foobar.db + +ACCESS_TOKEN_EXPIRATION_TIME=10H +REFRESH_TOKEN_EXPIRATION_TIME=2D + +INITIAL_USER_USERNAME +INITIAL_USER_PASSWORD +TOKEN_SECRET + START_WITH_STUDIO=false diff --git a/README.md b/README.md index bef6c93..bca0877 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Install Soul CLI with npm ## Usage +### 1. Running Soul + Soul is command line tool, after installing it, Run `soul -d sqlite.db -p 8000` and it'll start a REST API on [http://localhost:8000](http://localhost:8000) and a Websocket server on [ws://localhost:8000](ws://localhost:8000). @@ -27,13 +29,21 @@ Usage: soul [options] Options: - --version Show version number [boolean] - -d, --database SQLite database file or :memory: [string] [required] - -p, --port Port to listen on [number] - -r, --rate-limit-enabled Enable rate limiting [boolean] - -c, --cors CORS whitelist origins [string] - -S, --studio Start Soul Studio in parallel - --help Show help + --version Show version number [boolean] + -d, --database SQLite database file or :memory: [string] [required] + -p, --port Port to listen on [number] + -r, --rate-limit-enabled Enable rate limiting [boolean] + -c, --cors CORS whitelist origins [string] + -a, --auth Enable authentication and authorization [boolean] + + -iuu, --initialuserusername Initial user username [string] + -iup, --initialuserpassword Initial user password [string] + + -ts, --tokensecret Token Secret [string] + -atet, --accesstokenexpirationtime Access Token Expiration Time (Default: 5H) [string] + -rtet, --refreshtokenexpirationtime Refresh Token Expiration Time (Default: 1D) [string] + -S, --studio Start Soul Studio in parallel + --help Show help ``` @@ -45,6 +55,49 @@ curl http://localhost:8000/api/tables It should return a list of the tables inside `sqlite.db` database. +### 2. Running Soul in Auth mode + +To run Soul in auth mode, allowing login and signup features with authorization capabilities in your database tables, follow these steps: + +Run the Soul command with the necessary parameters: + +``` + + soul --d foobar.db -a -ts -atet=4H -rtet=3D -iuu=john -iup= + +``` + +Note: When configuring your JWT Secret, it is recommended to use a long string value for enhanced security. It is advisable to use a secret that is at least 10 characters in length. + +In this example: + +The `-a` flag instructs Soul to run in auth mode. +The `-ts` flag allows you to pass a JWT secret value for the `access and refresh tokens` generation and verification. Replace with your desired secret value. +The `-atet` flag sets the JWT expiration time for the access token. In this case, it is set to four hours (4H), meaning the token will expire after 4 hours. +The `-rtet` flag sets the JWT expiration time for the refresh token. In this case, it is set to three days (3D), meaning the token will expire after 3 days. +The `-iuu` flag is used to pass a username for the initial user +The `-iup` flag is used to pass a password for the initial user + +Here are some example values for the `-atet` and `rtet` flags + +- 60M: Represents a duration of 60 minutes. +- 5H: Represents a duration of 5 hours. +- 1D: Represents a duration of 1 day. + +NOTE: It is crucial to securely store a copy of the `-ts`(`Token Secret`) value used in Soul. Once you pass this values, make sure to keep a backup because you will need it every time you restart Soul. Losing this secret values can result in a situation where all of your users are blocked from accessing Soul. + +### 3. Updating Super Users + +To modify a superuser information in a database, you can utilize the `updatesuperuser` command. This command allows you to change a superuser's `password` or upgrade/downgrade a normal user to a `superuser`. Below is an example of how to use it: + +``` +soul --d foobar.db updatesuperuser --id=1 password= // Update the password for the superuser with ID 1 + +soul --d foobar.db updatesuperuser --id=1 --is_superuser=true // Upgrade the user with ID 1 to a superuser + +soul --d foobar.db updatesuperuser --id=1 --is_superuser=false // Revoke the superuser role from the superuser with ID 1 +``` + ## Documentation API documentation is available while the project is running at [http://localhost:8000/api/docs](http://localhost:8000/api/docs) @@ -63,8 +116,8 @@ A collection of projects that revolve around the Soul ecosystem. - [Soul Studio](https://github.com/thevahidal/soul-studio) provides a GUI to work with your database. - Right now Soul Studio is in early stages of development and not useful to work with. - + Right now Soul Studio is in early stages of development and not useful to work with. +

@@ -83,6 +136,14 @@ npm install # Install dependencies npm run dev # Start the dev server ``` +## Testing + +Set the `AUTH` variable to `true` in your `.env` file and use the command below to run the tests + +``` + npm run test +``` + ## Community [Join](https://bit.ly/soul-discord) the discussion in our Discord server and help making Soul together. @@ -91,7 +152,6 @@ npm run dev # Start the dev server [MIT](https://choosealicense.com/licenses/mit/) - ## Contributing Contributions are always welcome! diff --git a/package-lock.json b/package-lock.json index d21bd8c..fde3d3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,26 @@ { "name": "soul-cli", - "version": "0.6.1", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "soul-cli", - "version": "0.6.1", + "version": "0.7.0", "license": "MIT", "dependencies": { + "bcrypt": "^5.1.1", "better-sqlite3": "^8.1.0", "body-parser": "^1.20.2", + "check-password-strength": "^2.0.7", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.2", "express-rate-limit": "^6.7.0", "express-winston": "^4.2.0", "joi": "^17.8.3", + "jsonwebtoken": "^9.0.2", "soul-studio": "^0.0.1", "swagger-ui-express": "^4.6.1", "winston": "^3.8.2", @@ -32,7 +36,6 @@ "eslint-config-prettier": "^9.0.0", "husky": "^8.0.3", "jest": "^29.4.3", - "lint-staged": "^15.1.0", "nodemon": "^2.0.20", "prettier": "3.1.0", "supertest": "^6.3.3", @@ -1520,6 +1523,69 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "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/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/@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/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/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/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1718,8 +1784,7 @@ "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==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/accepts": { "version": "1.3.8", @@ -1754,6 +1819,38 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.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/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1817,6 +1914,23 @@ "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/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/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2012,8 +2126,7 @@ "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==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -2034,6 +2147,19 @@ } ] }, + "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/better-sqlite3": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.1.0.tgz", @@ -2098,7 +2224,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2176,6 +2301,11 @@ "ieee754": "^1.1.13" } }, + "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", @@ -2258,6 +2388,11 @@ "node": ">=10" } }, + "node_modules/check-password-strength": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/check-password-strength/-/check-password-strength-2.0.7.tgz", + "integrity": "sha512-VyklBkB6dOKnCIh63zdVr7QKVMN9/npwUqNAXxWrz8HabVZH/n/d+lyNm1O/vbXFJlT/Hytb5ouYKYGkoeZirQ==" + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -2311,87 +2446,6 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "dev": true, - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "dev": true, - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2452,11 +2506,13 @@ "simple-swizzle": "^0.2.2" } }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "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/colorspace": { "version": "1.1.4", @@ -2479,15 +2535,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -2497,8 +2544,12 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "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", @@ -2533,6 +2584,26 @@ "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", @@ -2648,6 +2719,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", @@ -2721,11 +2797,13 @@ "node": ">=12" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "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", @@ -3201,12 +3279,6 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3548,11 +3620,37 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, + "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", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.2", @@ -3573,6 +3671,25 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "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/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3633,7 +3750,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3712,6 +3828,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/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -3742,6 +3863,39 @@ "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/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3868,7 +4022,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -5764,357 +5917,131 @@ "node": ">=6" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "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": { - "json-buffer": "3.0.1" + "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/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, + "node_modules/jsonwebtoken/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": ">=6" + "node": ">=10" } }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "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/lint-staged": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.1.0.tgz", - "integrity": "sha512-ZPKXWHVlL7uwVpy8OZ7YQjYDAuO5X4kMh0XgZvPNxLcCCngd0PO5jKQyy3+s4TL2EnHoIXIzP1422f/l3nZKMw==", - "dev": true, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { - "chalk": "5.3.0", - "commander": "11.1.0", - "debug": "4.3.4", - "execa": "8.0.1", - "lilconfig": "2.1.0", - "listr2": "7.0.2", - "micromatch": "4.0.5", - "pidtree": "0.6.0", - "string-argv": "0.3.2", - "yaml": "2.3.4" + "lru-cache": "^6.0.0" }, "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/lint-staged/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/lint-staged/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/lint-staged/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" + "semver": "bin/semver.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/lint-staged/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10" } }, - "node_modules/lint-staged/node_modules/mimic-fn": { + "node_modules/jsonwebtoken/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "node_modules/lint-staged/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "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": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "node_modules/lint-staged/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/lint-staged/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" } }, - "node_modules/listr2": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-7.0.2.tgz", - "integrity": "sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { - "cli-truncate": "^3.1.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^5.0.1", - "rfdc": "^1.3.0", - "wrap-ansi": "^8.1.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "json-buffer": "3.0.1" } }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=6" } }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, - "node_modules/listr2/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">= 0.8.0" } }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "node_modules/locate-path": { "version": "5.0.0", @@ -6133,136 +6060,46 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/log-update": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", - "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", - "dev": true, - "dependencies": { - "ansi-escapes": "^5.0.0", - "cli-cursor": "^4.0.0", - "slice-ansi": "^5.0.0", - "strip-ansi": "^7.0.1", - "wrap-ansi": "^8.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", - "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", - "dev": true, - "dependencies": { - "type-fest": "^1.0.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "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/log-update/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } + "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/log-update/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "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/log-update/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "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/log-update/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "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/log-update/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } + "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/log-update/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } + "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/logform": { "version": "2.5.1", @@ -6295,7 +6132,6 @@ "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==", - "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -6409,7 +6245,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6425,6 +6260,53 @@ "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/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", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -6495,6 +6377,30 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "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-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6595,6 +6501,17 @@ "node": ">=8" } }, + "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", @@ -6772,7 +6689,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6815,18 +6731,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/pirates": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", @@ -7136,22 +7040,6 @@ "node": ">=10" } }, - "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7162,17 +7050,10 @@ "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -7242,7 +7123,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -7289,6 +7169,11 @@ "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/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -7331,8 +7216,7 @@ "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==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-concat": { "version": "1.0.1", @@ -7426,46 +7310,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/soul-studio": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/soul-studio/-/soul-studio-0.0.1.tgz", @@ -7541,15 +7385,6 @@ "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", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "engines": { - "node": ">=0.6.19" - } - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -7773,6 +7608,22 @@ "express": ">=4.0.0" } }, + "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/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -7799,6 +7650,19 @@ "node": ">=6" } }, + "node_modules/tar/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/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": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -7871,6 +7735,11 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/triple-beam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", @@ -8031,6 +7900,20 @@ "makeerror": "1.0.12" } }, + "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": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8046,6 +7929,14 @@ "node": ">= 8" } }, + "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/winston": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", @@ -8178,15 +8069,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "17.7.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", diff --git a/package.json b/package.json index 4cb18b0..4bb9690 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "soul-cli", - "version": "0.6.1", + "version": "0.7.0", "description": "A SQLite REST and Realtime server", "main": "src/server.js", "bin": { @@ -27,14 +27,18 @@ }, "homepage": "https://github.com/thevahidal/soul#readme", "dependencies": { + "bcrypt": "^5.1.1", "better-sqlite3": "^8.1.0", "body-parser": "^1.20.2", + "check-password-strength": "^2.0.7", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.2", "express-rate-limit": "^6.7.0", "express-winston": "^4.2.0", "joi": "^17.8.3", + "jsonwebtoken": "^9.0.2", "soul-studio": "^0.0.1", "swagger-ui-express": "^4.6.1", "winston": "^3.8.2", diff --git a/src/cli.js b/src/cli.js index 6745135..d18d773 100644 --- a/src/cli.js +++ b/src/cli.js @@ -46,11 +46,69 @@ if (process.env.NO_CLI !== 'true') { type: 'string', demandOption: false, }) + .options('a', { + alias: 'auth', + describe: 'Enable authentication and authorization', + type: 'boolean', + default: false, + demandOption: false, + }) + .options('ts', { + alias: 'tokensecret', + describe: 'JWT secret for the access and refresh tokens', + type: 'string', + default: null, + demandOption: false, + }) + .options('atet', { + alias: 'accesstokenexpirationtime', + describe: 'JWT expiration time for access token', + type: 'string', + default: '5H', + demandOption: false, + }) + .options('rtet', { + alias: 'refreshtokenexpirationtime', + describe: 'JWT expiration time for refresh token', + type: 'string', + default: '3D', + demandOption: false, + }) + .options('iuu', { + alias: 'initialuserusername', + describe: 'Initial superuser username', + type: 'string', + demandOption: false, + }) + .options('iup', { + alias: 'initialuserpassword', + describe: 'Initial superuser password', + type: 'string', + demandOption: false, + }) .options('S', { alias: 'studio', describe: 'Start Soul Studio in parallel', type: 'boolean', - demandOption: false + demandOption: false, + }) + .command('updatesuperuser', 'Update a superuser', (yargs) => { + return yargs + .option('id', { + describe: 'The ID of the superuser you want to update', + type: 'number', + demandOption: true, + }) + .option('password', { + describe: 'The new password for the superuser you want to update', + type: 'string', + demandOption: false, + }) + .option('is_superuser', { + describe: 'The role of the superuser you want to update', + type: 'boolean', + demandOption: false, + }); }) .help(true).argv; } diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 0000000..b798a30 --- /dev/null +++ b/src/commands.js @@ -0,0 +1,22 @@ +const { yargs } = require('./cli'); +const { updateSuperuser } = require('./controllers/auth'); + +const { argv } = yargs; + +const runCLICommands = () => { + // if the updatesuperuser command is passed from the CLI execute the updatesuperuser function + if (argv._.includes('updatesuperuser')) { + const { id, password, is_superuser } = argv; + + if (!password && !is_superuser) { + console.log( + 'Please provide either the --password or --is_superuser flag when using the updateuser command.', + ); + process.exit(1); + } else { + updateSuperuser({ id, password, is_superuser }); + } + } +}; + +module.exports = { runCLICommands }; diff --git a/src/config/index.js b/src/config/index.js index e282fcb..617e403 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -2,7 +2,7 @@ const dotenv = require('dotenv'); const Joi = require('joi'); const path = require('path'); -const { yargs, usage, options } = require('../cli'); +const { yargs } = require('../cli'); const { argv } = yargs; @@ -20,6 +20,7 @@ const envVarsSchema = Joi.object() VERBOSE: Joi.string().valid('console', null).default(null), CORS_ORIGIN_WHITELIST: Joi.string().default('*'), + AUTH: Joi.boolean().default(false), RATE_LIMIT_ENABLED: Joi.boolean().default(false), RATE_LIMIT_WINDOW_MS: Joi.number().positive().default(1000), @@ -27,7 +28,14 @@ const envVarsSchema = Joi.object() EXTENSIONS: Joi.string().default(null), - START_WITH_STUDIO: Joi.boolean().default(false) + START_WITH_STUDIO: Joi.boolean().default(false), + + INITIAL_USER_USERNAME: Joi.string(), + INITIAL_USER_PASSWORD: Joi.string(), + + TOKEN_SECRET: Joi.string().default(null), + ACCESS_TOKEN_EXPIRATION_TIME: Joi.string().default('5H'), + REFRESH_TOKEN_EXPIRATION_TIME: Joi.string().default('3D'), }) .unknown(); @@ -51,10 +59,34 @@ if (argv.cors) { env.CORS_ORIGIN_WHITELIST = argv.cors; } +if (argv.auth) { + env.AUTH = argv.auth; +} + if (argv['rate-limit-enabled']) { env.RATE_LIMIT_ENABLED = argv['rate-limit-enabled']; } +if (argv.tokensecret) { + env.TOKEN_SECRET = argv.tokensecret; +} + +if (argv.accesstokenexpirationtime) { + env.ACCESS_TOKEN_EXPIRATION_TIME = argv.accesstokenexpirationtime; +} + +if (argv.refreshtokenexpirationtime) { + env.REFRESH_TOKEN_EXPIRATION_TIME = argv.refreshtokenexpirationtime; +} + +if (argv.initialuserusername) { + env.INITIAL_USER_USERNAME = argv.initialuserusername; +} + +if (argv.initialuserpassword) { + env.INITIAL_USER_PASSWORD = argv.initialuserpassword; +} + const { value: envVars, error } = envVarsSchema .prefs({ errors: { label: 'key' } }) .validate(env); @@ -80,6 +112,19 @@ module.exports = { origin: argv.cors?.split(',') || envVars.CORS_ORIGIN_WHITELIST?.split(',') || ['*'], }, + + auth: argv.auth || envVars.AUTH, + tokenSecret: argv.tokensecret || envVars.TOKEN_SECRET, + accessTokenExpirationTime: + argv.accesstokenexpirationtime || envVars.ACCESS_TOKEN_EXPIRATION_TIME, + refreshTokenExpirationTime: + argv.refreshtokenexpirationtime || envVars.REFRESH_TOKEN_EXPIRATION_TIME, + + initialUserUsername: + argv.initialuserusername || envVars.INITIAL_USER_USERNAME, + initialUserPassword: + argv.initialuserpassword || envVars.INITIAL_USER_PASSWORD, + rateLimit: { enabled: argv['rate-limit-enabled'] || envVars.RATE_LIMIT_ENABLED, windowMs: envVars.RATE_LIMIT_WINDOW, @@ -90,5 +135,5 @@ module.exports = { path: argv.extensions || envVars.EXTENSIONS, }, - startWithStudio: argv.studio || envVars.START_WITH_STUDIO + startWithStudio: argv.studio || envVars.START_WITH_STUDIO, }; diff --git a/src/constants/api.js b/src/constants/api.js new file mode 100644 index 0000000..d32d917 --- /dev/null +++ b/src/constants/api.js @@ -0,0 +1,26 @@ +module.exports = { + authEndpoints: ['_users', '_roles', '_roles_permissions', '_users_roles'], + baseTableUrl: '/api/tables', + universalAccessEndpoints: ['/api/auth/change-password'], + + DEFAULT_PAGE_LIMIT: 10, + DEFAULT_PAGE_INDEX: 0, + PASSWORD: { + TOO_WEAK: 'Too weak', + WEAK: 'Weak', + }, + + httpVerbs: { + POST: 'POST', + GET: 'GET', + PUT: 'PUT', + DELETE: 'DELETE', + }, + + httpMethodDefinitions: { + POST: 'CREATE', + GET: 'READ', + PUT: 'UPDATE', + DELETE: 'DELETE', + }, +}; diff --git a/src/constants/auth.js b/src/constants/auth.js new file mode 100644 index 0000000..2c18c55 --- /dev/null +++ b/src/constants/auth.js @@ -0,0 +1,5 @@ +module.exports = { + SALT_ROUNDS: 10, + ACCESS_TOKEN_SUBJECT: 'accessToken', + REFRESH_TOKEN_SUBJECT: 'refreshToken', +}; diff --git a/src/constants/index.js b/src/constants/index.js new file mode 100644 index 0000000..d89805c --- /dev/null +++ b/src/constants/index.js @@ -0,0 +1,13 @@ +const dbConstants = require('./tables'); +const apiConstants = require('./api'); +const constantRoles = require('./roles'); +const responseMessages = require('./messages'); +const authConstants = require('./auth'); + +module.exports = { + dbConstants, + apiConstants, + constantRoles, + responseMessages, + authConstants, +}; diff --git a/src/constants/messages.js b/src/constants/messages.js new file mode 100644 index 0000000..0380680 --- /dev/null +++ b/src/constants/messages.js @@ -0,0 +1,41 @@ +module.exports = { + successMessage: { + SUCCESS: 'Success', + ROW_INSERTED: 'Row Inserted', + PASSWORD_UPDATE_SUCCESS: 'Password updated successfully', + USER_UPDATE_SUCCESS: 'User updated successfully', + INITIAL_USER_CREATED_SUCCESS: 'Initial user created successfully', + }, + + errorMessage: { + USERNAME_TAKEN_ERROR: 'This username is taken', + WEAK_PASSWORD_ERROR: 'This password is weak, please use another password', + DEFAULT_ROLE_NOT_CREATED_ERROR: + 'Please restart soul so a default role can be created', + INVALID_USERNAME_PASSWORD_ERROR: 'Invalid username or password', + INVALID_REFRESH_TOKEN_ERROR: 'Invalid refresh token', + INVALID_ACCESS_TOKEN_ERROR: 'Invalid access token', + USER_NOT_FOUND_ERROR: 'User not found', + INVALID_CURRENT_PASSWORD_ERROR: 'Invalid current password', + NOT_AUTHORIZED_ERROR: 'Not authorized', + PERMISSION_NOT_DEFINED_ERROR: 'Permission not defined for this role', + ROLE_NOT_FOUND_ERROR: 'Role not found for this user', + AUTH_SET_TO_FALSE_ERROR: + 'You can not access this endpoint while AUTH is set to false', + RESERVED_TABLE_NAME_ERROR: + 'The table name is reserved. Please choose a different name for the table.', + SERVER_ERROR: 'Server error', + + INITIAL_USER_USERNAME_NOT_PASSED_ERROR: + 'Error: You should pass the initial users username either from the CLI with the --iuu or from the environment variable using the INITIAL_USER_USERNAME flag', + INITIAL_USER_PASSWORD_NOT_PASSED_ERROR: + 'Error: You should pass the initial users password either from the CLI with the --iup or from the environment variable using the INITIAL_USER_PASSWORD flag', + + USERNAME_REQUIRED_ERROR: 'username is required', + PASSWORD_REQUIRED_ERROR: 'password is required', + }, + + infoMessage: { + INITIAL_USER_ALREADY_CREATED: 'Initial user is already created', + }, +}; diff --git a/src/constants/roles.js b/src/constants/roles.js new file mode 100644 index 0000000..d798827 --- /dev/null +++ b/src/constants/roles.js @@ -0,0 +1,3 @@ +module.exports = { + DEFAULT_ROLE: 'default', +}; diff --git a/src/constants/tables.js b/src/constants/tables.js new file mode 100644 index 0000000..d2c1772 --- /dev/null +++ b/src/constants/tables.js @@ -0,0 +1,47 @@ +const USERS_TABLE = '_users'; +const ROLES_TABLE = '_roles'; +const USERS_ROLES_TABLE = '_users_roles'; +const ROLES_PERMISSIONS_TABLE = '_roles_permissions'; + +module.exports = { + USERS_TABLE, + ROLES_TABLE, + USERS_ROLES_TABLE, + ROLES_PERMISSIONS_TABLE, + + reservedTableNames: [ + USERS_TABLE, + ROLES_TABLE, + USERS_ROLES_TABLE, + ROLES_PERMISSIONS_TABLE, + ], + + constraints: { + UNIQUE_USERS_ROLE: 'unique_users_role', + UNIQUE_ROLES_TABLE: 'unique_ROLES_TABLE', + }, + + tableFields: { + ID: 'id', + + // _role fields + ROLE_NAME: 'name', + + // _user fields + USERNAME: 'username', + HASHED_PASSWORD: 'hashed_password', + SALT: 'salt', + IS_SUPERUSER: 'is_superuser', + + // _roles_permissions fields + ROLE_ID: 'role_id', + TABLE_NAME: 'table_name', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete', + + // _users_roles fields + USER_ID: 'user_id', + }, +}; diff --git a/src/controllers/auth.test.js b/src/controllers/auth.test.js new file mode 100644 index 0000000..ec38345 --- /dev/null +++ b/src/controllers/auth.test.js @@ -0,0 +1,372 @@ +const supertest = require('supertest'); + +const app = require('../index'); +const config = require('../config'); +const { generateToken } = require('../utils'); +const { testData } = require('../tests/testData'); + +const requestWithSupertest = supertest(app); + +describe('Auth Endpoints', () => { + describe('User Endpoints', () => { + it('POST /tables/_users/rows should register a user', async () => { + const accessToken = await generateToken( + { username: 'John', userId: 1, isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .post('/api/tables/_users/rows') + .set('Cookie', [`accessToken=${accessToken}`]) + .send({ + fields: { + username: testData.users.user1.username, + password: testData.strongPassword, + }, + }); + + expect(res.status).toEqual(201); + expect(res.type).toEqual(expect.stringContaining('json')); + + expect(res.body).toHaveProperty('message'); + expect(res.body.message).toBe('Row Inserted'); + + expect(res.body).not.toHaveProperty('password'); + expect(res.body).not.toHaveProperty('hashed_password'); + expect(res.body).not.toHaveProperty('salt'); + }); + + it('POST /tables/_users/rows should throw 400 error if username is not passed', async () => { + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .post('/api/tables/_users/rows') + .set('Cookie', [`accessToken=${accessToken}`]) + .send({ + fields: { password: testData.strongPassword }, + }); + + expect(res.status).toEqual(400); + expect(res.body.message).toBe('username is required'); + + expect(res.body).not.toHaveProperty('password'); + expect(res.body).not.toHaveProperty('hashed_password'); + expect(res.body).not.toHaveProperty('salt'); + }); + + it('POST /tables/_users/rows should throw 400 error if the password is not strong', async () => { + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .post('/api/tables/_users/rows') + .set('Cookie', [`accessToken=${accessToken}`]) + .send({ + fields: { + username: testData.users.user2.username, + password: testData.weakPassword, + }, + }); + + expect(res.status).toEqual(400); + expect(res.body.message).toBe( + 'This password is weak, please use another password', + ); + + expect(res.body).not.toHaveProperty('password'); + expect(res.body).not.toHaveProperty('hashed_password'); + expect(res.body).not.toHaveProperty('salt'); + }); + + it('POST /tables/_users/rows should throw 409 error if the username is taken', async () => { + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .post('/api/tables/_users/rows') + .set('Cookie', [`accessToken=${accessToken}`]) + .send({ + fields: { + username: testData.users.user1.username, + password: testData.strongPassword, + }, + }); + + expect(res.status).toEqual(409); + expect(res.body.message).toBe('This username is taken'); + + expect(res.body).not.toHaveProperty('password'); + expect(res.body).not.toHaveProperty('hashed_password'); + expect(res.body).not.toHaveProperty('salt'); + }); + + it('GET /tables/_users/rows should return list of users', async () => { + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .get('/api/tables/_users/rows') + .set('Cookie', [`accessToken=${accessToken}`]); + + expect(res.status).toEqual(200); + expect(res.body.data[0]).toHaveProperty('id'); + expect(res.body.data[0]).toHaveProperty('username'); + expect(res.body.data[0]).toHaveProperty('is_superuser'); + expect(res.body.data[0]).toHaveProperty('createdAt'); + + expect(res.body.data[0]).not.toHaveProperty('password'); + expect(res.body.data[0]).not.toHaveProperty('hashed_password'); + expect(res.body.data[0]).not.toHaveProperty('salt'); + }); + + it('GET /tables/_users/rows/:id should retrive a single user', async () => { + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .get('/api/tables/_users/rows/1') + .set('Cookie', [`accessToken=${accessToken}`]); + + expect(res.status).toEqual(200); + expect(res.body.data[0]).toHaveProperty('id'); + expect(res.body.data[0]).toHaveProperty('username'); + expect(res.body.data[0]).toHaveProperty('is_superuser'); + expect(res.body.data[0]).toHaveProperty('createdAt'); + + expect(res.body.data[0]).not.toHaveProperty('password'); + expect(res.body.data[0]).not.toHaveProperty('hashed_password'); + expect(res.body.data[0]).not.toHaveProperty('salt'); + }); + + it('PUT /tables/_users/rows/:id should update a user', async () => { + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .put('/api/tables/_users/rows/1') + .set('Cookie', [`accessToken=${accessToken}`]) + .send({ + fields: { + username: testData.users.user3.username, + }, + }); + + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining('json')); + + expect(res.body).toHaveProperty('message'); + expect(res.body.message).toBe('Row updated'); + + expect(res.body).not.toHaveProperty('password'); + expect(res.body).not.toHaveProperty('hashed_password'); + expect(res.body).not.toHaveProperty('salt'); + }); + + it('DELETE /tables/_users/rows/:id should remove a user', async () => { + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .delete('/api/tables/_users/rows/2') + .set('Cookie', [`accessToken=${accessToken}`]); + + expect(res.status).toEqual(400); + expect(res.body.message).toBe('FOREIGN KEY constraint failed'); + + expect(res.body).not.toHaveProperty('password'); + expect(res.body).not.toHaveProperty('hashed_password'); + expect(res.body).not.toHaveProperty('salt'); + }); + }); + + describe('Obtain Access Token Endpoint', () => { + it('POST /auth/token/obtain should return an access token and refresh token values and a success message', async () => { + const res = await requestWithSupertest + .post('/api/auth/token/obtain') + .send({ + fields: { + username: testData.users.user1.username, + password: testData.strongPassword, + }, + }); + + expect(res.status).toEqual(201); + expect(res.type).toEqual(expect.stringContaining('json')); + expect(res.body).toHaveProperty('message'); + expect(res.body.message).toBe('Success'); + + expect(res.headers['set-cookie']).toBeDefined(); + expect(res.headers['set-cookie']).toEqual( + expect.arrayContaining([ + expect.stringContaining('refreshToken='), + expect.stringContaining('accessToken='), + ]), + ); + + expect(res.body).not.toHaveProperty('password'); + expect(res.body).not.toHaveProperty('hashed_password'); + expect(res.body).not.toHaveProperty('salt'); + }); + + it('POST /auth/token/obtain should throw a 401 error if the username does not exist in the DB', async () => { + const res = await requestWithSupertest + .post('/api/auth/token/obtain') + .send({ + fields: { + username: testData.invalidUsername, + password: testData.strongPassword, + }, + }); + + expect(res.status).toEqual(401); + expect(res.type).toEqual(expect.stringContaining('json')); + expect(res.body).toHaveProperty('message'); + expect(res.body.message).toBe('Invalid username or password'); + + expect(res.body).not.toHaveProperty('password'); + expect(res.body).not.toHaveProperty('hashed_password'); + expect(res.body).not.toHaveProperty('salt'); + }); + + it('POST /auth/token/obtain should throw a 401 error if the password is invalid', async () => { + const res = await requestWithSupertest + .post('/api/auth/token/obtain') + .send({ + fields: { + username: testData.users.user1.username, + password: testData.invalidPassword, + }, + }); + + expect(res.status).toEqual(401); + expect(res.type).toEqual(expect.stringContaining('json')); + expect(res.body).toHaveProperty('message'); + expect(res.body.message).toBe('Invalid username or password'); + + expect(res.body).not.toHaveProperty('password'); + expect(res.body).not.toHaveProperty('hashed_password'); + expect(res.body).not.toHaveProperty('salt'); + }); + }); + + describe('Refresh Access Token Endpoint', () => { + it('GET /auth/token/refresh should refresh the access and refresh tokens', async () => { + const refreshToken = await generateToken( + { username: 'John', userId: 1, isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .get('/api/auth/token/refresh') + .set('Cookie', [`refreshToken=${refreshToken}`]); + + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining('json')); + expect(res.body).toHaveProperty('message'); + expect(res.body.message).toBe('Success'); + + expect(res.headers['set-cookie']).toBeDefined(); + expect(res.headers['set-cookie']).toEqual( + expect.arrayContaining([ + expect.stringContaining('refreshToken='), + expect.stringContaining('accessToken='), + ]), + ); + }); + }); + + describe('Change Password Endpoint', () => { + it('PUT /auth/change-password/ should change a password', async () => { + const accessToken = await generateToken( + { username: 'John', userId: 2, isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .put('/api/auth/change-password') + .set('Cookie', [`accessToken=${accessToken}`]) + .send({ + fields: { + currentPassword: testData.strongPassword, + newPassword: testData.strongPassword2, + }, + }); + + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining('json')); + expect(res.body).toHaveProperty('message'); + expect(res.body.message).toBe('Password updated successfully'); + + expect(res.body).not.toHaveProperty('password'); + expect(res.body).not.toHaveProperty('hashed_password'); + expect(res.body).not.toHaveProperty('salt'); + + // check if the password is really updated + const res2 = await requestWithSupertest + .post('/api/auth/token/obtain') + .send({ + fields: { + username: testData.users.user1.username, + password: testData.strongPassword2, + }, + }); + + expect(res2.status).toEqual(201); + expect(res2.type).toEqual(expect.stringContaining('json')); + expect(res2.body).toHaveProperty('message'); + expect(res2.body.message).toBe('Success'); + }); + + it('PUT /auth/change-password/ should throw 401 error if the current password is not valid', async () => { + const accessToken = await generateToken( + { username: 'John', userId: 2, isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .put('/api/auth/change-password') + .set('Cookie', [`accessToken=${accessToken}`]) + .send({ + fields: { + currentPassword: testData.invalidPassword, + newPassword: testData.strongPassword2, + }, + }); + + expect(res.status).toEqual(401); + expect(res.type).toEqual(expect.stringContaining('json')); + expect(res.body).toHaveProperty('message'); + expect(res.body.message).toBe('Invalid current password'); + + expect(res.body).not.toHaveProperty('password'); + expect(res.body).not.toHaveProperty('hashed_password'); + expect(res.body).not.toHaveProperty('salt'); + }); + }); +}); diff --git a/src/controllers/auth/common.js b/src/controllers/auth/common.js new file mode 100644 index 0000000..777930d --- /dev/null +++ b/src/controllers/auth/common.js @@ -0,0 +1,16 @@ +const { rowService } = require('../../services'); +const { dbConstants } = require('../../constants'); + +const { USERS_TABLE } = dbConstants; + +const isUsernameTaken = (username) => { + const users = rowService.get({ + tableName: USERS_TABLE, + whereString: 'WHERE username=?', + whereStringValues: [username], + }); + + return users.length > 0; +}; + +module.exports = { isUsernameTaken }; diff --git a/src/controllers/auth/index.js b/src/controllers/auth/index.js new file mode 100644 index 0000000..d7e3c44 --- /dev/null +++ b/src/controllers/auth/index.js @@ -0,0 +1,5 @@ +const users = require('./user'); +const token = require('./token'); +const tables = require('./tables'); + +module.exports = { ...users, ...token, ...tables }; diff --git a/src/controllers/auth/tables.js b/src/controllers/auth/tables.js new file mode 100644 index 0000000..0dbdab0 --- /dev/null +++ b/src/controllers/auth/tables.js @@ -0,0 +1,96 @@ +const { tableService, rowService } = require('../../services'); +const { constantRoles, dbConstants } = require('../../constants'); +const schema = require('../../db/schema'); + +const { + USERS_TABLE, + ROLES_TABLE, + USERS_ROLES_TABLE, + ROLES_PERMISSIONS_TABLE, + constraints, + tableFields, +} = dbConstants; + +const createDefaultTables = async () => { + let roleId; + + // check if the default tables are already created + const roleTable = tableService.checkTableExists(ROLES_TABLE); + const usersTable = tableService.checkTableExists(USERS_TABLE); + const rolesPermissionTable = tableService.checkTableExists( + ROLES_PERMISSIONS_TABLE, + ); + const usersRolesTable = tableService.checkTableExists(USERS_ROLES_TABLE); + + // create _users table + if (!usersTable) { + tableService.createTable(USERS_TABLE, schema.userSchema); + } + + // create _users_roles table + if (!usersRolesTable) { + tableService.createTable( + USERS_ROLES_TABLE, + + schema.usersRoleSchema, + { + multipleUniqueConstraints: { + name: constraints.UNIQUE_USERS_ROLE, + fields: [tableFields.USER_ID, tableFields.USER_ID], + }, + }, + ); + } + + // create _roles table + if (!roleTable) { + tableService.createTable(ROLES_TABLE, schema.roleSchema); + + // create a default role in the _roles table + const role = rowService.save({ + tableName: ROLES_TABLE, + fields: { name: constantRoles.DEFAULT_ROLE }, + }); + roleId = role.lastInsertRowid; + } + + // create _roles_permissions table + if (!rolesPermissionTable && roleId) { + tableService.createTable( + ROLES_PERMISSIONS_TABLE, + schema.rolePermissionSchema, + { + multipleUniqueConstraints: { + name: constraints.UNIQUE_ROLES_TABLE, + fields: [tableFields.ROLE_ID, tableFields.TABLE_NAME], + }, + }, + ); + + // fetch all DB tables + const tables = tableService.listTables(); + + // add permission for the default role (for each db table) + const permissions = []; + for (const table of tables) { + permissions.push({ + role_id: roleId, + table_name: table.name, + create: 'false', + read: 'true', + update: 'false', + delete: 'false', + }); + } + + // store the permissions in the db + rowService.bulkWrite({ + tableName: ROLES_PERMISSIONS_TABLE, + fields: permissions, + }); + } +}; + +module.exports = { + createDefaultTables, +}; diff --git a/src/controllers/auth/token.js b/src/controllers/auth/token.js new file mode 100644 index 0000000..cd0e74f --- /dev/null +++ b/src/controllers/auth/token.js @@ -0,0 +1,243 @@ +const { authService } = require('../../services'); +const { responseMessages, authConstants } = require('../../constants'); +const config = require('../../config'); +const { + comparePasswords, + generateToken, + decodeToken, + toBoolean, +} = require('../../utils'); + +const { successMessage, errorMessage } = responseMessages; + +const obtainAccessToken = async (req, res) => { + /* + #swagger.tags = ['Auth'] + #swagger.summary = 'Obtain Access Token' + #swagger.description = 'Endpoint to generate access and refresh tokens' + + #swagger.parameters['body'] = { + in: 'body', + required: true, + type: 'object', + schema: { $ref: '#/definitions/ObtainAccessTokenRequestBody' } + } + */ + + // extract payload + const { username, password } = req.body.fields; + + try { + // check if the username exists in the Db + const users = authService.getUsersByUsername({ username }); + + if (users.length <= 0) { + return res + .status(401) + .send({ message: errorMessage.INVALID_USERNAME_PASSWORD_ERROR }); + } + + // check if the password is valid + const user = users[0]; + const isMatch = await comparePasswords(password, user.hashed_password); + + if (!isMatch) { + return res + .status(401) + .send({ message: errorMessage.INVALID_USERNAME_PASSWORD_ERROR }); + /* + #swagger.responses[401] = { + description: 'Invalid username or password error', + schema: { + $ref: '#/definitions/InvalidCredentialErrorResponse' + } + } + */ + } + + let permissions, roleIds; + + // if the user is not a superuser get the role and its permission from the DB + if (!toBoolean(user.is_superuser)) { + const roleData = getUsersRoleAndPermission({ + userId: user.id, + res, + }); + + permissions = roleData.permissions; + roleIds = roleData.roleIds; + } + + const payload = { + username: user.username, + userId: user.id, + isSuperuser: user.is_superuser, + roleIds, + permissions, + }; + + // generate an access token + const accessToken = await generateToken( + { subject: authConstants.ACCESS_TOKEN_SUBJECT, ...payload }, + config.tokenSecret, + config.accessTokenExpirationTime, + ); + + // generate a refresh token + const refreshToken = await generateToken( + { subject: authConstants.REFRESH_TOKEN_SUBJECT, ...payload }, + config.tokenSecret, + config.refreshTokenExpirationTime, + ); + + // set the token in the cookie + let cookieOptions = { httpOnly: true, secure: false, Path: '/' }; + res.cookie(authConstants.ACCESS_TOKEN_SUBJECT, accessToken, cookieOptions); + res.cookie( + authConstants.REFRESH_TOKEN_SUBJECT, + refreshToken, + cookieOptions, + ); + + res + .status(201) + .send({ message: successMessage.SUCCESS, data: { userId: user.id } }); + + /* + #swagger.responses[201] = { + description: 'Access token and Refresh token generated', + schema: { + $ref: '#/definitions/ObtainAccessTokenSuccessResponse' + } + } + */ + } catch (error) { + console.log(error); + return res.status(500).json({ + message: errorMessage.SERVER_ERROR, + }); + } +}; + +const refreshAccessToken = async (req, res) => { + /* + #swagger.tags = ['Auth'] + #swagger.summary = 'Refresh Access Token' + #swagger.description = 'Endpoint to refresh access and refresh tokens' + */ + + try { + // extract the payload from the token and verify it + const payload = await decodeToken( + req.cookies.refreshToken, + config.tokenSecret, + ); + + // find the user + const users = authService.getUsersById({ userId: payload.userId }); + + if (users.length <= 0) { + return res + .status(401) + .send({ message: errorMessage.USER_NOT_FOUND_ERROR }); + + /* + #swagger.responses[401] = { + description: 'User not found error', + schema: { + $ref: '#/definitions/UserNotFoundErrorResponse' + } + } + */ + } + + let permissions, roleIds; + const user = users[0]; + + // if the user is not a superuser get the role and its permission from the DB + if (!toBoolean(user.is_superuser)) { + const roleData = getUsersRoleAndPermission({ + userId: user.id, + res, + }); + + permissions = roleData.permissions; + roleIds = roleData.roleIds; + } + + const newPayload = { + username: user.username, + userId: user.id, + isSuperuser: user.is_superuser, + roleIds, + permissions, + }; + + // generate an access token + const accessToken = await generateToken( + { subject: authConstants.ACCESS_TOKEN_SUBJECT, ...newPayload }, + config.tokenSecret, + config.accessTokenExpirationTime, + ); + + // generate a refresh token + const refreshToken = await generateToken( + { subject: authConstants.REFRESH_TOKEN_SUBJECT, ...newPayload }, + config.tokenSecret, + config.refreshTokenExpirationTime, + ); + + // set the token in the cookie + let cookieOptions = { httpOnly: true, secure: false, Path: '/' }; + res.cookie(authConstants.ACCESS_TOKEN_SUBJECT, accessToken, cookieOptions); + res.cookie( + authConstants.REFRESH_TOKEN_SUBJECT, + refreshToken, + cookieOptions, + ); + + res + .status(200) + .send({ message: successMessage.SUCCESS, data: { userId: user.id } }); + + /* + #swagger.responses[200] = { + description: 'Access token refreshed', + schema: { + $ref: '#/definitions/RefreshAccessTokenSuccessResponse' + } + } + */ + } catch (error) { + res.status(403).send({ message: errorMessage.INVALID_REFRESH_TOKEN_ERROR }); + /* + #swagger.responses[401] = { + description: 'Invalid refresh token error', + schema: { + $ref: '#/definitions/InvalidRefreshTokenErrorResponse' + } + } + */ + } +}; + +const getUsersRoleAndPermission = ({ userId, res }) => { + const userRoles = authService.getUserRoleByUserId({ userId }); + + if (userRoles <= 0) { + res.status(401).send({ message: errorMessage.ROLE_NOT_FOUND_ERROR }); + throw new Error(errorMessage.ROLE_NOT_FOUND_ERROR); + } + + const roleIds = userRoles.map((role) => role.role_id); + + // get the permission of the role + const permissions = authService.getPermissionByRoleIds({ roleIds }); + + return { userRoles, roleIds, permissions }; +}; + +module.exports = { + obtainAccessToken, + refreshAccessToken, +}; diff --git a/src/controllers/auth/user.js b/src/controllers/auth/user.js new file mode 100644 index 0000000..c7eccbc --- /dev/null +++ b/src/controllers/auth/user.js @@ -0,0 +1,394 @@ +const { rowService, authService } = require('../../services'); +const { + apiConstants, + dbConstants, + responseMessages, + authConstants, +} = require('../../constants'); +const config = require('../../config'); +const { + hashPassword, + checkPasswordStrength, + comparePasswords, +} = require('../../utils'); + +const { USERS_TABLE, USERS_ROLES_TABLE, tableFields } = dbConstants; + +const { SALT_ROUNDS } = authConstants; + +const { successMessage, errorMessage, infoMessage } = responseMessages; + +const updateSuperuser = async (fields) => { + const { id, password, is_superuser } = fields; + let newHashedPassword, newSalt; + let fieldsString = ''; + + try { + // find the user by using the id field + const users = authService.getRoleByUserId({ id }); + + // abort if the id is invalid + if (users.length === 0) { + console.log(errorMessage.USER_NOT_FOUND_ERROR); + process.exit(1); + } + + // check if the is_superuser field is passed + if (is_superuser !== undefined) { + fieldsString = `${tableFields.IS_SUPERUSER} = '${is_superuser}'`; + } + + // if the password is sent from the CLI, update it + if (password) { + // check if the password is weak + if ( + [apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes( + checkPasswordStrength(password), + ) + ) { + console.log(errorMessage.WEAK_PASSWORD_ERROR); + process.exit(1); + } + + //hash the password + const { hashedPassword, salt } = await hashPassword( + password, + SALT_ROUNDS, + ); + newHashedPassword = hashedPassword; + newSalt = salt; + + fieldsString = `${fieldsString ? fieldsString + ', ' : ''} ${ + tableFields.HASHED_PASSWORD + } = '${newHashedPassword}', ${tableFields.SALT} = '${newSalt}'`; + } + + // update the user + rowService.update({ + tableName: USERS_TABLE, + lookupField: tableFields.ID, + fieldsString, + pks: `${id}`, + }); + + console.log(successMessage.USER_UPDATE_SUCCESS); + process.exit(1); + } catch (error) { + console.log(error); + } +}; + +const registerUser = async (req, res) => { + /* + #swagger.tags = ['Auth'] + #swagger.summary = 'Register User' + #swagger.description = 'Endpoint to signup' + + #swagger.parameters['username'] = { + in: 'body', + required: true, + type: 'object', + schema: { $ref: '#/definitions/UserRegistrationRequestBody' } + } + */ + + const { username, password } = req.body.fields; + + try { + if (!username) { + return res + .status(400) + .send({ message: errorMessage.USERNAME_REQUIRED_ERROR }); + } + + if (!password) { + return res + .status(400) + .send({ message: errorMessage.PASSWORD_REQUIRED_ERROR }); + } + + // check if the username is taken + const users = authService.getUsersByUsername({ username }); + + if (users.length > 0) { + return res + .status(409) + .send({ message: errorMessage.USERNAME_TAKEN_ERROR }); + + /* + #swagger.responses[409] = { + description: 'Username taken error', + schema: { + $ref: '#/definitions/UsernameTakenErrorResponse' + } + } + */ + } + + // check if the password is weak + if ( + [apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes( + checkPasswordStrength(password), + ) + ) { + return res.status(400).send({ + message: errorMessage.WEAK_PASSWORD_ERROR, + }); + + /* + #swagger.responses[400] = { + description: 'Weak password error', + schema: { + $ref: '#/definitions/WeakPasswordErrorResponse' + } + } + */ + } + + // hash the password + const { salt, hashedPassword } = await hashPassword(password, SALT_ROUNDS); + + // create the user + const newUser = rowService.save({ + tableName: USERS_TABLE, + fields: { + username, + salt, + hashed_password: hashedPassword, + is_superuser: 'false', + }, + }); + + // find the default role from the DB + const defaultRole = authService.getDefaultRole(); + + if (defaultRole.length <= 0) { + return res.status(500).send({ + message: errorMessage.DEFAULT_ROLE_NOT_CREATED_ERROR, + }); + /* + #swagger.responses[500] = { + description: 'Server error', + schema: { + $ref: '#/definitions/DefaultRoleNotCreatedErrorResponse' + } + } + */ + } + + // create a role for the user + rowService.save({ + tableName: USERS_ROLES_TABLE, + fields: { user_id: newUser.lastInsertRowid, role_id: defaultRole[0].id }, + }); + + res.status(201).send({ message: successMessage.ROW_INSERTED }); + + /* + #swagger.responses[201] = { + description: 'Row inserted', + schema: { + $ref: '#/definitions/InsertRowSuccessResponse' + } + } + */ + } catch (error) { + console.log(error); + res.status(500).send({ message: errorMessage.SERVER_ERROR }); + } +}; + +const changePassword = async (req, res) => { + /* + #swagger.tags = ['Auth'] + #swagger.summary = 'Change Password' + #swagger.description = 'Endpoint to change a password' + + #swagger.parameters['body'] = { + in: 'body', + required: true, + type: 'object', + schema: { + $ref: '#/definitions/ChangePasswordRequestBody' + } + } + */ + + const userInfo = req.user; + const { currentPassword, newPassword } = req.body.fields; + + try { + // get the user from the Db + const users = authService.getUsersById({ userId: userInfo.userId }); + + if (users.length <= 0) { + return res + .status(401) + .send({ message: errorMessage.USER_NOT_FOUND_ERROR }); + } + + const user = users[0]; + + // check if the users current password is valid + const isMatch = await comparePasswords( + currentPassword, + user.hashed_password, + ); + + if (!isMatch) { + return res + .status(401) + .send({ message: errorMessage.INVALID_CURRENT_PASSWORD_ERROR }); + /* + #swagger.responses[401] = { + description: 'User not found error', + schema: { + $ref: '#/definitions/InvalidPasswordErrorResponse' + } + } + */ + } + + // check if the new password is strong + if ( + [apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes( + checkPasswordStrength(newPassword), + ) + ) { + return res.status(400).send({ + message: errorMessage.WEAK_PASSWORD_ERROR, + }); + + /* + #swagger.responses[400] = { + description: 'Weak password error', + schema: { + $ref: '#/definitions/WeakPasswordErrorResponse' + } + } + */ + } + + // hash the password + const { salt, hashedPassword } = await hashPassword( + newPassword, + SALT_ROUNDS, + ); + + user.salt = salt; + user.hashed_password = hashedPassword; + + // update the user + rowService.update({ + tableName: USERS_TABLE, + lookupField: tableFields.ID, + fieldsString: `${tableFields.HASHED_PASSWORD} = '${hashedPassword}', ${tableFields.SALT} = '${salt}'`, + pks: `${user.id}`, + }); + + res.status(200).send({ + message: successMessage.PASSWORD_UPDATE_SUCCESS, + data: { id: user.id, username: user.username }, + }); + + /* + #swagger.responses[200] = { + description: 'Weak password error', + schema: { + $ref: '#/definitions/ChangePasswordSuccessResponse' + } + } + */ + } catch (error) { + res.status(500).send({ message: errorMessage.SERVER_ERROR }); + } +}; + +const createInitialUser = async () => { + // extract some fields from the environment variables or from the CLI + const { initialUserUsername: username, initialUserPassword: password } = + config; + + try { + // check if there are users in the DB + const users = authService.getAllUsers(); + + if (users.length <= 0) { + // check if initial users username is passed from the env or CLI + if (!username) { + console.error(errorMessage.INITIAL_USER_USERNAME_NOT_PASSED_ERROR); + process.exit(1); + } + + // check if initial users password is passed from the env or CLI + if (!password) { + console.error(errorMessage.INITIAL_USER_PASSWORD_NOT_PASSED_ERROR); + process.exit(1); + } + + // check if the usernmae is taken + const users = authService.getUsersByUsername({ username }); + + if (users.length > 0) { + console.error(errorMessage.USERNAME_TAKEN_ERROR); + process.exit(1); + } + + // check if the password is strong + if ( + [apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes( + checkPasswordStrength(password), + ) + ) { + console.error(errorMessage.WEAK_PASSWORD_ERROR); + process.exit(1); + } + + // hash the password + const { hashedPassword, salt } = await hashPassword( + password, + SALT_ROUNDS, + ); + + // create the initial user + const { lastInsertRowid: userId } = rowService.save({ + tableName: USERS_TABLE, + fields: { + username, + hashed_password: hashedPassword, + salt, + is_superuser: 'false', + }, + }); + + // get the default role from the DB + const roles = authService.getDefaultRole(); + + if (roles.length <= 0) { + console.log(errorMessage.DEFAULT_ROLE_NOT_CREATED_ERROR); + process.exit(1); + } + + const defaultRoleId = roles[0].id; + + // create a _users_role for the initial user + rowService.save({ + tableName: USERS_ROLES_TABLE, + fields: { user_id: userId, role_id: defaultRoleId }, + }); + + console.log(successMessage.INITIAL_USER_CREATED_SUCCESS); + } else { + console.log(infoMessage.INITIAL_USER_ALREADY_CREATED); + } + } catch (error) { + console.log(error); + } +}; + +module.exports = { + updateSuperuser, + registerUser, + changePassword, + createInitialUser, +}; diff --git a/src/controllers/rows.js b/src/controllers/rows.js index f45761b..7d31c26 100644 --- a/src/controllers/rows.js +++ b/src/controllers/rows.js @@ -1,12 +1,6 @@ const db = require('../db/index'); const { rowService } = require('../services'); -const quotePrimaryKeys = (pks) => { - const primaryKeys = pks.split(','); - const quotedPks = primaryKeys.map((id) => `'${id}'`).join(','); - return quotedPks; -}; - const operators = { eq: '=', lt: '<', @@ -15,11 +9,11 @@ const operators = { gte: '>=', neq: '!=', null: 'IS NULL', - notnull: 'IS NOT NULL' + notnull: 'IS NOT NULL', }; // Return paginated rows of a table -const listTableRows = async (req, res) => { +const listTableRows = async (req, res, next) => { /* #swagger.tags = ['Rows'] #swagger.summary = 'List Rows' @@ -66,7 +60,7 @@ const listTableRows = async (req, res) => { _ordering, _schema, _extend, - _filters = '' + _filters = '', } = req.query; const page = parseInt(_page); @@ -80,7 +74,7 @@ const listTableRows = async (req, res) => { let filters = []; // split the filters by comma(,) except when in an array - const re = /,(?![^\[]*?\])/; + const re = /,(?![^[]*?\])/; try { filters = _filters.split(re).map((filter) => { //NOTE: When using the _filter parameter, the values are split using the ":" sign, like this (_filters=Total__eq:1). However, if the user sends a date value, such as (_filters=InvoiceDate__eq:2010-01-08 00:00:00), there will be additional colon (":") signs present. @@ -98,7 +92,7 @@ const listTableRows = async (req, res) => { fieldOperator = 'eq'; } else if (!operators[fieldOperator]) { throw new Error( - `Invalid field operator '${fieldOperator}' for field '${field}'. You can only use the following operators after the '${field}' field: __lt, __gt, __lte, __gte, __eq, __neq.` + `Invalid field operator '${fieldOperator}' for field '${field}'. You can only use the following operators after the '${field}' field: __lt, __gt, __lte, __gte, __eq, __neq.`, ); } @@ -112,7 +106,7 @@ const listTableRows = async (req, res) => { } catch (error) { return res.status(400).json({ message: error.message, - error: error + error: error, }); } @@ -161,7 +155,7 @@ const listTableRows = async (req, res) => { } catch (error) { return res.status(400).json({ message: error.message, - error: error + error: error, }); } } @@ -236,7 +230,7 @@ const listTableRows = async (req, res) => { if (!foreignKey) { throw new Error( - `Foreign key not found for extended field '${extendedField}'` + `Foreign key not found for extended field '${extendedField}'`, ); } @@ -254,7 +248,7 @@ const listTableRows = async (req, res) => { joinedTableFields .map( (joinedTableField) => - `'${joinedTableField.name}', ${joinedTableName}.${joinedTableField.name}` + `'${joinedTableField.name}', ${joinedTableName}.${joinedTableField.name}`, ) .join(', ') + ' ) as ' + @@ -277,7 +271,7 @@ const listTableRows = async (req, res) => { if (foreignKeyError.error) { return res.status(400).json({ message: foreignKeyError.message, - error: foreignKeyError.error + error: foreignKeyError.error, }); } } @@ -291,7 +285,7 @@ const listTableRows = async (req, res) => { orderString, limit, page: limit * (page - 1), - whereStringValues + whereStringValues, }); // parse json extended files @@ -311,10 +305,10 @@ const listTableRows = async (req, res) => { const total = rowService.getCount({ tableName, whereString, - whereStringValues + whereStringValues, }); - const next = + const nextPage = data.length === limit ? `/tables/${tableName}/rows?${params}_limit=${_limit}&_page=${ page + 1 @@ -327,16 +321,15 @@ const listTableRows = async (req, res) => { }` : null; - res.json({ - data, - total, - next, - previous - }); + req.response = { + status: 200, + payload: { data, total, next: nextPage, previous }, + }; + next(); } catch (error) { res.status(400).json({ message: error.message, - error: error + error: error, }); } }; @@ -367,7 +360,7 @@ const insertRowInTable = async (req, res, next) => { // Remove null values from fields for accurate query construction. const fields = Object.fromEntries( - Object.entries(queryFields).filter(([_, value]) => value !== null) + Object.entries(queryFields).filter(([, value]) => value !== null), ); try { @@ -383,14 +376,15 @@ const insertRowInTable = async (req, res, next) => { */ res.status(201).json({ message: 'Row inserted', - data + data, }); + req.broadcast = { type: 'INSERT', data: { pk: data.lastInsertRowid, - ...fields - } + ...fields, + }, }; next(); } catch (error) { @@ -404,13 +398,13 @@ const insertRowInTable = async (req, res, next) => { */ res.status(400).json({ message: error.message, - error: error + error: error, }); } }; // Get a row by pk -const getRowInTableByPK = async (req, res) => { +const getRowInTableByPK = async (req, res, next) => { /* #swagger.tags = ['Rows'] #swagger.summary = 'Retrieve Row' @@ -465,7 +459,7 @@ const getRowInTableByPK = async (req, res) => { } catch (error) { return res.status(400).json({ message: error.message, - error: error + error: error, }); } } @@ -542,7 +536,7 @@ const getRowInTableByPK = async (req, res) => { joinedTableFields .map( (joinedTableField) => - `'${joinedTableField.name}', ${joinedTableName}.${joinedTableField.name}` + `'${joinedTableField.name}', ${joinedTableName}.${joinedTableField.name}`, ) .join(', ') + ' ) as ' + @@ -557,7 +551,7 @@ const getRowInTableByPK = async (req, res) => { } catch (error) { return res.status(400).json({ message: error.message, - error: error + error: error, }); } }); @@ -569,7 +563,7 @@ const getRowInTableByPK = async (req, res) => { tableName, extendString, lookupField, - pks + pks, }); // parse json extended files @@ -588,17 +582,16 @@ const getRowInTableByPK = async (req, res) => { if (data.length === 0) { return res.status(404).json({ message: 'Row not found', - error: 'not_found' + error: 'not_found', }); } else { - res.json({ - data - }); + req.response = { status: 200, payload: { data } }; + next(); } } catch (error) { return res.status(400).json({ message: error.message, - error: error + error: error, }); } }; @@ -652,7 +645,7 @@ const updateRowInTableByPK = async (req, res, next) => { } catch (error) { return res.status(400).json({ message: error.message, - error: error + error: error, }); } } @@ -671,7 +664,7 @@ const updateRowInTableByPK = async (req, res, next) => { if (fieldsString === '') { return res.status(400).json({ message: 'No fields provided', - error: 'no_fields_provided' + error: 'no_fields_provided', }); } @@ -680,26 +673,26 @@ const updateRowInTableByPK = async (req, res, next) => { tableName, fieldsString, lookupField, - pks + pks, }); res.json({ message: 'Row updated', - data + data, }); req.broadcast = { type: 'UPDATE', _lookup_field: lookupField, data: { pks: pks.split(','), - ...fields - } + ...fields, + }, }; next(); } catch (error) { res.status(400).json({ message: error.message, - error: error + error: error, }); } }; @@ -744,7 +737,7 @@ const deleteRowInTableByPK = async (req, res, next) => { } catch (error) { return res.status(400).json({ message: error.message, - error: error + error: error, }); } } @@ -754,26 +747,26 @@ const deleteRowInTableByPK = async (req, res, next) => { if (data.changes === 0) { res.status(404).json({ - error: 'not_found' + error: 'not_found', }); } else { res.json({ message: 'Row deleted', - data + data, }); req.broadcast = { type: 'DELETE', _lookup_field: lookupField, data: { - pks: pks.split(',') - } + pks: pks.split(','), + }, }; next(); } } catch (error) { res.status(400).json({ message: error.message, - error: error + error: error, }); } }; @@ -783,5 +776,5 @@ module.exports = { insertRowInTable, getRowInTableByPK, updateRowInTableByPK, - deleteRowInTableByPK + deleteRowInTableByPK, }; diff --git a/src/controllers/rows.test.js b/src/controllers/rows.test.js index d5ef038..4509993 100644 --- a/src/controllers/rows.test.js +++ b/src/controllers/rows.test.js @@ -2,6 +2,9 @@ const { not } = require('joi'); const supertest = require('supertest'); const app = require('../index'); +const config = require('../config'); +const { generateToken } = require('../utils'); + const requestWithSupertest = supertest(app); function queryString(params) { @@ -14,7 +17,15 @@ function queryString(params) { describe('Rows Endpoints', () => { it('GET /tables/:name/rows should return a list of all rows', async () => { - const res = await requestWithSupertest.get('/api/tables/users/rows'); + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .get('/api/tables/users/rows') + .set('Cookie', [`accessToken=${accessToken}`]); expect(res.status).toEqual(200); expect(res.type).toEqual(expect.stringContaining('json')); @@ -26,17 +37,23 @@ describe('Rows Endpoints', () => { }); it('GET /tables/:name/rows?_limit=8&_schema=firstName,lastName&_ordering:-firstName&_page=2: should query the rows by the provided query params', async () => { + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + const params = { _search: 'a', _ordering: '-firstName', _schema: 'firstName,lastName', _limit: 8, - _page: 2 + _page: 2, }; const query = queryString(params); - const res = await requestWithSupertest.get( - `/api/tables/users/rows?${query}` - ); + const res = await requestWithSupertest + .get(`/api/tables/users/rows?${query}`) + .set('Cookie', [`accessToken=${accessToken}`]); expect(res.status).toEqual(200); expect(res.type).toEqual(expect.stringContaining('json')); @@ -48,34 +65,46 @@ describe('Rows Endpoints', () => { expect(res.body.next).toEqual( `/tables/users/rows?${queryString({ ...params, - _page: params._page + 1 - }).toString()}` + _page: params._page + 1, + }).toString()}`, ); expect(res.body.previous).toEqual( `/tables/users/rows?${queryString({ ...params, - _page: params._page - 1 - }).toString()}` + _page: params._page - 1, + }).toString()}`, ); }); it('GET /tables/:name/rows: should return a null field', async () => { - const res = await requestWithSupertest.get( - '/api/tables/users/rows?_filters=firstName__null,lastName__notnull' + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', ); + const res = await requestWithSupertest + .get('/api/tables/users/rows?_filters=firstName__null,lastName__notnull') + .set('Cookie', [`accessToken=${accessToken}`]); + expect(res.status).toEqual(200); expect(res.body.data[0].firstName).toBeNull(); expect(res.body.data[0].lastName).not.toBeNull(); }); it('GET /tables/:name/rows: should successfully retrieve users created after 2010-01-01 00:00:00.', async () => { - const date = '2010-01-01 00:00:00'; - const res = await requestWithSupertest.get( - `/api/tables/users/rows?_filters=createdAt__gte:${date}` + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', ); + const date = '2010-01-01 00:00:00'; + const res = await requestWithSupertest + .get(`/api/tables/users/rows?_filters=createdAt__gte:${date}`) + .set('Cookie', [`accessToken=${accessToken}`]); + res.body.data.map((user) => { const createdAt = new Date(user.createdAt); const referenceDate = new Date(date); @@ -90,11 +119,17 @@ describe('Rows Endpoints', () => { }); it('GET /tables/:name/rows: should successfully retrieve users created before 2008-01-20 00:00:00.', async () => { - const date = '2008-01-20 00:00:00'; - const res = await requestWithSupertest.get( - `/api/tables/users/rows?_filters=createdAt__lte:${date}` + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', ); + const date = '2008-01-20 00:00:00'; + const res = await requestWithSupertest + .get(`/api/tables/users/rows?_filters=createdAt__lte:${date}`) + .set('Cookie', [`accessToken=${accessToken}`]); + res.body.data.map((user) => { const createdAt = new Date(user.createdAt); const referenceDate = new Date(date); @@ -109,11 +144,17 @@ describe('Rows Endpoints', () => { }); it('GET /tables/:name/rows: should successfully retrieve users created at 2013-01-08 00:00:00', async () => { - const date = '2013-01-08 00:00:00'; - const res = await requestWithSupertest.get( - `/api/tables/users/rows?_filters=createdAt__eq:${date}` + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', ); + const date = '2013-01-08 00:00:00'; + const res = await requestWithSupertest + .get(`/api/tables/users/rows?_filters=createdAt__eq:${date}`) + .set('Cookie', [`accessToken=${accessToken}`]); + res.body.data.map((user) => { const createdAt = new Date(user.createdAt); const referenceDate = new Date(date); @@ -128,22 +169,34 @@ describe('Rows Endpoints', () => { }); it('GET /tables/:name/rows: should successfully retrieve users created at 2007-01-08 00:00:00', async () => { - const date = '2007-01-08 00:00:00'; - const res = await requestWithSupertest.get( - `/api/tables/users/rows?_filters=createdAt__eq:${date}` + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', ); + const date = '2007-01-08 00:00:00'; + const res = await requestWithSupertest + .get(`/api/tables/users/rows?_filters=createdAt__eq:${date}`) + .set('Cookie', [`accessToken=${accessToken}`]); + //There are no users that are created at 2007-01-08 00:00:00 so the API should return empty data expect(res.body.data).toHaveLength(0); expect(res.status).toEqual(200); }); it('GET /tables/:name/rows: should successfully retrieve users that are not created at 2021-01-08 00:00:00', async () => { - const date = '2021-01-08 00:00:00'; - const res = await requestWithSupertest.get( - `/api/tables/users/rows?_filters=createdAt__neq:${date}` + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', ); + const date = '2021-01-08 00:00:00'; + const res = await requestWithSupertest + .get(`/api/tables/users/rows?_filters=createdAt__neq:${date}`) + .set('Cookie', [`accessToken=${accessToken}`]); + res.body.data.map((user) => { const createdAt = new Date(user.createdAt); const referenceDate = new Date(date); @@ -158,16 +211,33 @@ describe('Rows Endpoints', () => { }); it('POST /tables/:name/rows should insert a new row and return the lastInsertRowid', async () => { + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + const res = await requestWithSupertest .post('/api/tables/users/rows') + .set('Cookie', [`accessToken=${accessToken}`]) .send({ fields: { firstName: 'Jane', lastName: 'Doe' } }); + expect(res.status).toEqual(201); expect(res.type).toEqual(expect.stringContaining('json')); expect(res.body).toHaveProperty('data'); }); it('GET /tables/:name/rows/:pks should return a row by its primary key', async () => { - const res = await requestWithSupertest.get('/api/tables/users/rows/1'); + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .get('/api/tables/users/rows/1') + .set('Cookie', [`accessToken=${accessToken}`]); + expect(res.status).toEqual(200); expect(res.type).toEqual(expect.stringContaining('json')); expect(res.body).toHaveProperty('data'); @@ -177,37 +247,65 @@ describe('Rows Endpoints', () => { }); it('PUT /tables/:name/rows/:pks should update a row by its primary key and return the number of changes', async () => { + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); const res = await requestWithSupertest .put('/api/tables/users/rows/1') + .set('Cookie', [`accessToken=${accessToken}`]) .send({ fields: { firstName: 'Jane', lastName: 'Doe' } }); expect(res.status).toEqual(200); expect(res.type).toEqual(expect.stringContaining('json')); }); it('DELETE /tables/:name/rows/:pks should delete a row by its primary key and return the number of changes', async () => { - const res = await requestWithSupertest.delete('/api/tables/users/rows/1'); + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .delete('/api/tables/users/rows/1') + .set('Cookie', [`accessToken=${accessToken}`]); expect(res.status).toEqual(200); expect(res.type).toEqual(expect.stringContaining('json')); }); it('POST /tables/:name/rows should insert a new row if any of the value of the object being inserted is null', async () => { - const res = await requestWithSupertest.post('/api/tables/users/rows').send({ - fields: { - firstName: null, - lastName: 'Doe', - email: null, - username: 'Jane' - } - }); + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + const res = await requestWithSupertest + .post('/api/tables/users/rows') + .send({ + fields: { + firstName: null, + lastName: 'Doe', + email: null, + username: 'Jane', + }, + }) + .set('Cookie', [`accessToken=${accessToken}`]); expect(res.status).toEqual(201); expect(res.type).toEqual(expect.stringContaining('json')); expect(res.body).toHaveProperty('data'); }); it('GET /tables/:name/rows should return values if any of the IDs from the array match the user ID.', async () => { - const res = await requestWithSupertest.get( - '/api/tables/users/rows?_filters=id:[2,3]' + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', ); + + const res = await requestWithSupertest + .get('/api/tables/users/rows?_filters=id:[2,3]') + .set('Cookie', [`accessToken=${accessToken}`]); expect(res.status).toEqual(200); expect(res.body).toHaveProperty('data'); expect(res.body.data).toEqual(expect.any(Array)); @@ -215,9 +313,17 @@ describe('Rows Endpoints', () => { }); it('GET /tables/:name/rows should return values if the provided ID matches the user ID.', async () => { - const res = await requestWithSupertest.get( - '/api/tables/users/rows?_filters=id:2,firstName:Michael,lastName:Lee' + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', ); + + const res = await requestWithSupertest + .get( + '/api/tables/users/rows?_filters=id:2,firstName:Michael,lastName:Lee', + ) + .set('Cookie', [`accessToken=${accessToken}`]); expect(res.status).toEqual(200); expect(res.body).toHaveProperty('data'); expect(res.body.data).toEqual(expect.any(Array)); diff --git a/src/controllers/tables.test.js b/src/controllers/tables.test.js index 3a9f371..6a89fe6 100644 --- a/src/controllers/tables.test.js +++ b/src/controllers/tables.test.js @@ -1,11 +1,23 @@ const supertest = require('supertest'); const app = require('../index'); +const { generateToken } = require('../utils'); +const config = require('../config'); + const requestWithSupertest = supertest(app); describe('Tables Endpoints', () => { it('GET /tables should return a list of all tables', async () => { - const res = await requestWithSupertest.get('/api/tables'); + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .get('/api/tables') + .set('Cookie', [`accessToken=${accessToken}`]); + expect(res.status).toEqual(200); expect(res.type).toEqual(expect.stringContaining('json')); expect(res.body).toHaveProperty('data'); @@ -14,33 +26,42 @@ describe('Tables Endpoints', () => { }); it('POST /tables should create a new table and return generated schema', async () => { - const res = await requestWithSupertest.post('/api/tables').send({ - name: 'pets', - autoAddCreatedAt: true, - autoAddUpdatedAt: false, - schema: [ - { - name: 'owner', - type: 'INTEGER', - foreignKey: { - table: 'users', - column: 'id', - onDelete: 'CASCADE', - onUpdate: 'CASCADE', + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .post('/api/tables') + .send({ + name: 'pets', + autoAddCreatedAt: true, + autoAddUpdatedAt: false, + schema: [ + { + name: 'owner', + type: 'INTEGER', + foreignKey: { + table: 'users', + column: 'id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, }, - }, - { - name: 'name', - type: 'TEXT', - notNull: true, - }, - { - name: 'petId', - unique: true, - type: 'INTEGER', - }, - ], - }); + { + name: 'name', + type: 'TEXT', + notNull: true, + }, + { + name: 'petId', + unique: true, + type: 'INTEGER', + }, + ], + }) + .set('Cookie', [`accessToken=${accessToken}`]); expect(res.status).toEqual(201); expect(res.type).toEqual(expect.stringContaining('json')); @@ -53,7 +74,16 @@ describe('Tables Endpoints', () => { }); it('GET /tables/:name should return schema of the table', async () => { - const res = await requestWithSupertest.get('/api/tables/users'); + const accessToken = await generateToken( + { username: 'John', isSuperuser: true }, + config.tokenSecret, + '1H', + ); + + const res = await requestWithSupertest + .get('/api/tables/users') + .set('Cookie', [`accessToken=${accessToken}`]); + expect(res.status).toEqual(200); expect(res.type).toEqual(expect.stringContaining('json')); expect(res.body).toHaveProperty('data'); diff --git a/src/db/schema.js b/src/db/schema.js new file mode 100644 index 0000000..2b51f88 --- /dev/null +++ b/src/db/schema.js @@ -0,0 +1,118 @@ +const { dbConstants } = require('../constants'); + +const { tableFields, ROLES_TABLE, USERS_TABLE } = dbConstants; + +module.exports = { + roleSchema: [ + { + name: tableFields.ROLE_NAME, + type: 'TEXT', + primaryKey: false, + notNull: true, + unique: true, + }, + ], + + userSchema: [ + { + name: tableFields.USERNAME, + type: 'TEXT', + primaryKey: false, + notNull: true, + unique: true, + }, + { + name: tableFields.HASHED_PASSWORD, + type: 'TEXT', + primaryKey: false, + notNull: true, + unique: false, + }, + { + name: tableFields.SALT, + type: 'TEXT', + primaryKey: false, + notNull: true, + unique: false, + }, + + { + name: tableFields.IS_SUPERUSER, + type: 'BOOLEAN', + primaryKey: false, + notNull: true, + unique: false, + }, + ], + + rolePermissionSchema: [ + { + name: tableFields.ROLE_ID, + type: 'NUMERIC', + primaryKey: false, + notNull: true, + unique: false, + foreignKey: { table: ROLES_TABLE, column: tableFields.ID }, + }, + + { + name: tableFields.TABLE_NAME, + type: 'TEXT', + primaryKey: false, + notNull: true, + unique: false, + }, + + { + name: tableFields.CREATE, + type: 'BOOLEAN', + primaryKey: false, + notNull: true, + unique: false, + }, + + { + name: tableFields.READ, + type: 'BOOLEAN', + primaryKey: false, + notNull: true, + unique: false, + }, + + { + name: tableFields.UPDATE, + type: 'BOOLEAN', + primaryKey: false, + notNull: true, + unique: false, + }, + + { + name: tableFields.DELETE, + type: 'BOOLEAN', + primaryKey: false, + notNull: true, + unique: false, + }, + ], + + usersRoleSchema: [ + { + name: tableFields.USER_ID, + type: 'NUMERIC', + primaryKey: false, + notNull: true, + unique: false, + foreignKey: { table: USERS_TABLE, column: tableFields.ID }, + }, + + { + name: tableFields.ROLE_ID, + type: 'NUMERIC', + primaryKey: false, + notNull: true, + unique: false, + foreignKey: { table: ROLES_TABLE, column: tableFields.ID }, + }, + ], +}; diff --git a/src/index.js b/src/index.js index b91b43a..efcc9af 100755 --- a/src/index.js +++ b/src/index.js @@ -1,37 +1,47 @@ #! /usr/bin/env node -const express = require("express"); -const bodyParser = require("body-parser"); -const winston = require("winston"); -const expressWinston = require("express-winston"); -const cors = require("cors"); -const rateLimit = require("express-rate-limit"); -const swaggerUi = require("swagger-ui-express"); - -const config = require("./config/index"); -const db = require("./db/index"); -const rootRoutes = require("./routes/index"); -const tablesRoutes = require("./routes/tables"); -const rowsRoutes = require("./routes/rows"); -const swaggerFile = require("./swagger/swagger.json"); -const { setupExtensions } = require("./extensions"); +const express = require('express'); +const bodyParser = require('body-parser'); +const winston = require('winston'); +const expressWinston = require('express-winston'); +const cors = require('cors'); +const rateLimit = require('express-rate-limit'); +const swaggerUi = require('swagger-ui-express'); +const cookieParser = require('cookie-parser'); + +const config = require('./config/index'); +const db = require('./db/index'); + +const rootRoutes = require('./routes/index'); +const tablesRoutes = require('./routes/tables'); +const rowsRoutes = require('./routes/rows'); +const authRoutes = require('./routes/auth'); + +const swaggerFile = require('./swagger/swagger.json'); +const { setupExtensions } = require('./extensions'); +const { + createDefaultTables, + createInitialUser, +} = require('./controllers/auth'); + +const { runCLICommands } = require('./commands'); const app = express(); - -app.get("/health", (req, res) => { - res.send("OK"); +app.get('/health', (req, res) => { + res.send('OK'); }); app.use(bodyParser.json()); +app.use(cookieParser()); // Activate wal mode -db.exec("PRAGMA journal_mode = WAL"); +db.exec('PRAGMA journal_mode = WAL'); // Enable CORS let corsOrigin = config.cors.origin; -if (corsOrigin.includes("*")) { - corsOrigin = "*"; +if (corsOrigin.includes('*')) { + corsOrigin = '*'; } const corsOptions = { origin: corsOrigin }; @@ -49,13 +59,14 @@ if (config.verbose !== null) { winston.format.json(), ), meta: false, - msg: "HTTP {{req.method}} {{req.url}}", + msg: 'HTTP {{req.method}} {{req.url}}', expressFormat: true, colorize: false, }), ); } + if (config.rateLimit.enabled) { const limiter = rateLimit({ windowMs: config.rateLimit.windowMs, @@ -68,10 +79,25 @@ if (config.rateLimit.enabled) { app.use(limiter); } -app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); -app.use("/api", rootRoutes); -app.use("/api/tables", tablesRoutes); -app.use("/api/tables", rowsRoutes); +// If Auth mode is activated then create auth tables in the DB & create a super user if there are no users in the DB +if (config.auth) { + createDefaultTables(); + createInitialUser(); +} else { + console.warn( + 'Warning: Soul is running in open mode without authentication or authorization for API endpoints. Please be aware that your API endpoints will not be secure.', + ); +} + +// If the user has passed custom CLI commands run the command and exit to avoid running the server +runCLICommands(); + +app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)); +app.use('/api', rootRoutes); +app.use('/api/tables', tablesRoutes); +app.use('/api/tables', rowsRoutes); + +app.use('/api/auth', authRoutes); setupExtensions(app, db); diff --git a/src/middlewares/api.js b/src/middlewares/api.js new file mode 100644 index 0000000..4b47a9a --- /dev/null +++ b/src/middlewares/api.js @@ -0,0 +1,79 @@ +const config = require('../config'); +const { registerUser } = require('../controllers/auth'); +const { + apiConstants, + dbConstants, + responseMessages, +} = require('../constants/'); +const { removeFields } = require('../utils'); + +const { httpVerbs } = apiConstants; +const { reservedTableNames, USERS_TABLE, tableFields } = dbConstants; +const { errorMessage } = responseMessages; + +const processRowRequest = async (req, res, next) => { + const resource = req.params.name; + const { method } = req; + + // If the user sends a request to the auth tables while AUTH is set to false, throw an error + if (apiConstants.authEndpoints.includes(resource) && !config.auth) { + return res.status(403).send({ + message: errorMessage.AUTH_SET_TO_FALSE_ERROR, + }); + } + + // Redirect this request to the registerUser controller => POST /api/tables/_users/rows + if (resource === USERS_TABLE && method === httpVerbs.POST) { + return registerUser(req, res); + } + + // Remove some fields for this request and check the username field => PUT /api/tables/_users/rows + if (resource === USERS_TABLE && method === httpVerbs.PUT) { + /** + * remove some user fields from the request like (is_superuser, hashed_password, salt). + * NOTE: password can be updated via the /change-password API and superuser status can be only updated from the CLI + */ + removeFields( + [req.body.fields], + [tableFields.SALT, tableFields.IS_SUPERUSER, tableFields.HASHED_PASSWORD], + ); + } + + next(); +}; + +const processRowResponse = async (req, res, next) => { + // Extract payload data + const resource = req.params.name; + const status = req.response.status; + const payload = req.response.payload; + + // Remove some fields from the response + if (resource === USERS_TABLE) { + removeFields(payload.data, [tableFields.SALT, tableFields.HASHED_PASSWORD]); + } + + res.status(status).send(payload); + next(); +}; + +const processTableRequest = async (req, res, next) => { + const { method, body, baseUrl } = req; + + // if the user tries to create a table with the reserved table names throw an error. Request => POST /api/tables + if (baseUrl === apiConstants.baseTableUrl && method === httpVerbs.POST) { + if (reservedTableNames.includes(body.name)) { + return res.status(409).send({ + message: errorMessage.RESERVED_TABLE_NAME_ERROR, + }); + } + } + + next(); +}; + +module.exports = { + processRowRequest, + processRowResponse, + processTableRequest, +}; diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js new file mode 100644 index 0000000..ec74577 --- /dev/null +++ b/src/middlewares/auth.js @@ -0,0 +1,85 @@ +const config = require('../config'); +const { decodeToken, toBoolean } = require('../utils/index'); +const { apiConstants, responseMessages } = require('../constants'); + +const { errorMessage } = responseMessages; + +const hasAccess = async (req, res, next) => { + let payload; + const { name: tableName } = req.params; + const verb = req.method; + const originalURL = req.originalUrl; + + try { + if (config.auth) { + // extract the payload from the token and verify it + try { + payload = await decodeToken( + req.cookies.accessToken, + config.tokenSecret, + ); + req.user = payload; + } catch (error) { + return res + .status(401) + .send({ message: errorMessage.INVALID_ACCESS_TOKEN_ERROR }); + } + + // if the user is a super_user, allow access on the resource + if (toBoolean(payload.isSuperuser)) { + return next(); + } + + // if the endpoint is set to be accessed by any user regardless of there roles, then allow access + if (apiConstants.universalAccessEndpoints.includes(originalURL)) { + return next(); + } + + // if table_name is not passed from the router throw unauthorized error + if (!tableName) { + return res + .status(403) + .send({ message: errorMessage.NOT_AUTHORIZED_ERROR }); + } + + // if the user is not a super user, check the users permission on the resource + const permissions = payload.permissions.filter((row) => { + return row.table_name === tableName; + }); + + if (permissions.length <= 0) { + return res + .status(403) + .send({ message: errorMessage.PERMISSION_NOT_DEFINED_ERROR }); + } + + // If the user has permission on the table in at least in one of the roles then allow access on the table + let hasPermission = false; + + permissions.some((resource) => { + const httpMethod = + apiConstants.httpMethodDefinitions[verb].toLowerCase(); + + if (toBoolean(resource[httpMethod])) { + hasPermission = true; + return true; + } + }); + + if (hasPermission) { + next(); + } else { + return res + .status(403) + .send({ message: errorMessage.NOT_AUTHORIZED_ERROR }); + } + } else { + next(); + } + } catch (error) { + console.log(error); + res.status(401).send({ message: error.message }); + } +}; + +module.exports = { hasAccess }; diff --git a/src/middlewares/broadcast.js b/src/middlewares/broadcast.js index 74afcc0..bd48a7b 100644 --- a/src/middlewares/broadcast.js +++ b/src/middlewares/broadcast.js @@ -1,6 +1,6 @@ const { websocketSubscribers } = require('../websocket'); -const broadcast = (req, res, next) => { +const broadcast = (req) => { const data = req.broadcast; const { name: tableName } = req.params; diff --git a/src/middlewares/validation.js b/src/middlewares/validation.js index df18fdd..c8bcd69 100644 --- a/src/middlewares/validation.js +++ b/src/middlewares/validation.js @@ -1,6 +1,6 @@ const validator = (schema) => (req, res, next) => { - const { body, params, query } = req; - const data = { body, params, query }; + const { body, params, query, cookies } = req; + const data = { body, params, query, cookies }; const { value, error } = schema.validate(data); @@ -13,6 +13,7 @@ const validator = (schema) => (req, res, next) => { req.body = value.body; req.params = value.params; req.query = value.query; + req.cookies = value.cookies; next(); } diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..d238938 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,29 @@ +const express = require('express'); + +const controllers = require('../controllers/auth'); +const { validator } = require('../middlewares/validation'); +const schema = require('../schemas/auth'); +const { hasAccess } = require('../middlewares/auth'); + +const router = express.Router(); + +router.post( + '/token/obtain', + validator(schema.obtainAccessToken), + controllers.obtainAccessToken, +); + +router.get( + '/token/refresh', + validator(schema.refreshAccessToken), + controllers.refreshAccessToken, +); + +router.put( + '/change-password', + hasAccess, + validator(schema.changePassword), + controllers.changePassword, +); + +module.exports = router; diff --git a/src/routes/rows.js b/src/routes/rows.js index 3d3f69a..d5fcdc9 100644 --- a/src/routes/rows.js +++ b/src/routes/rows.js @@ -3,37 +3,49 @@ const express = require('express'); const controllers = require('../controllers/rows'); const { broadcast } = require('../middlewares/broadcast'); const { validator } = require('../middlewares/validation'); +const { processRowRequest, processRowResponse } = require('../middlewares/api'); +const { hasAccess } = require('../middlewares/auth'); const schema = require('../schemas/rows'); const router = express.Router(); router.get( '/:name/rows', + hasAccess, validator(schema.listTableRows), - controllers.listTableRows + processRowRequest, + controllers.listTableRows, + processRowResponse, ); router.post( '/:name/rows', + hasAccess, validator(schema.insertRowInTable), + processRowRequest, controllers.insertRowInTable, - broadcast + broadcast, ); router.get( '/:name/rows/:pks', + hasAccess, validator(schema.getRowInTableByPK), - controllers.getRowInTableByPK + controllers.getRowInTableByPK, + processRowResponse, ); router.put( '/:name/rows/:pks', + hasAccess, validator(schema.updateRowInTableByPK), + processRowRequest, controllers.updateRowInTableByPK, - broadcast + broadcast, ); router.delete( '/:name/rows/:pks', + hasAccess, validator(schema.deleteRowInTableByPK), controllers.deleteRowInTableByPK, - broadcast + broadcast, ); module.exports = router; diff --git a/src/routes/tables.js b/src/routes/tables.js index 0500a73..e75dbdc 100644 --- a/src/routes/tables.js +++ b/src/routes/tables.js @@ -3,16 +3,38 @@ const express = require('express'); const controllers = require('../controllers/tables'); const { validator } = require('../middlewares/validation'); const schema = require('../schemas/tables'); +const { hasAccess } = require('../middlewares/auth'); +const { processTableRequest } = require('../middlewares/api'); const router = express.Router(); -router.get('/', validator(schema.listTables), controllers.listTables); -router.post('/', validator(schema.createTable), controllers.createTable); +router.get( + '/', + hasAccess, + validator(schema.listTables), + controllers.listTables, +); + +router.post( + '/', + processTableRequest, + hasAccess, + validator(schema.createTable), + controllers.createTable, +); + router.get( '/:name', + hasAccess, validator(schema.getTableSchema), - controllers.getTableSchema + controllers.getTableSchema, +); + +router.delete( + '/:name', + hasAccess, + validator(schema.deleteTable), + controllers.deleteTable, ); -router.delete('/:name', validator(schema.deleteTable), controllers.deleteTable); module.exports = router; diff --git a/src/schemas/auth.js b/src/schemas/auth.js new file mode 100644 index 0000000..369edf2 --- /dev/null +++ b/src/schemas/auth.js @@ -0,0 +1,67 @@ +const Joi = require('joi'); + +const obtainAccessToken = Joi.object({ + query: Joi.object().required(), + params: Joi.object({}).required(), + + body: Joi.object({ + fields: Joi.object({ + username: Joi.string().required(), + password: Joi.string().required(), + }).required(), + }).required(), + + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), +}); + +const refreshAccessToken = Joi.object({ + query: Joi.object().required(), + params: Joi.object({}).required(), + body: Joi.object({}).required(), + cookies: Joi.object({ + refreshToken: Joi.string().required(), + accessToken: Joi.string().optional(), + }).required(), +}); + +const changePassword = Joi.object({ + query: Joi.object().required(), + params: Joi.object().required(), + + body: Joi.object({ + fields: Joi.object({ + currentPassword: Joi.string().required(), + newPassword: Joi.string().required(), + }).required(), + }).required(), + cookies: Joi.object({ + accessToken: Joi.string().required(), + refreshToken: Joi.string().optional(), + }).required(), +}); + +const registerUser = Joi.object({ + query: Joi.object().required(), + params: Joi.object({}).required(), + body: Joi.object({ + fields: Joi.object({ + username: Joi.string().required(), + password: Joi.string().required(), + }).required(), + }).required(), + + cookies: Joi.object({ + accessToken: Joi.string().required(), + refreshToken: Joi.string().optional(), + }).required(), +}); + +module.exports = { + obtainAccessToken, + refreshAccessToken, + changePassword, + registerUser, +}; diff --git a/src/schemas/index.js b/src/schemas/index.js index 5673219..8ce3ec3 100644 --- a/src/schemas/index.js +++ b/src/schemas/index.js @@ -12,10 +12,15 @@ const transaction = Joi.object({ }), Joi.object({ query: Joi.string().required(), - }) + }), ) .required(), }).required(), + + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); module.exports = { diff --git a/src/schemas/rows.js b/src/schemas/rows.js index 48502f4..d0e953a 100644 --- a/src/schemas/rows.js +++ b/src/schemas/rows.js @@ -14,6 +14,10 @@ const listTableRows = Joi.object({ name: Joi.string(), }).required(), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const insertRowInTable = Joi.object({ @@ -28,6 +32,10 @@ const insertRowInTable = Joi.object({ body: Joi.object({ fields: Joi.object().required(), }).required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const getRowInTableByPK = Joi.object({ @@ -48,6 +56,10 @@ const getRowInTableByPK = Joi.object({ pks: Joi.string().required(), }).required(), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const updateRowInTableByPK = Joi.object({ @@ -68,6 +80,10 @@ const updateRowInTableByPK = Joi.object({ body: Joi.object({ fields: Joi.object().required(), }).required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const deleteRowInTableByPK = Joi.object({ @@ -86,6 +102,10 @@ const deleteRowInTableByPK = Joi.object({ pks: Joi.string().required(), }).required(), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); module.exports = { diff --git a/src/schemas/tables.js b/src/schemas/tables.js index d35d078..e4cb082 100644 --- a/src/schemas/tables.js +++ b/src/schemas/tables.js @@ -7,6 +7,10 @@ const listTables = Joi.object({ }).required(), params: Joi.object().required(), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const createTable = Joi.object({ @@ -38,7 +42,7 @@ const createTable = Joi.object({ 'BLOB', 'BOOLEAN', 'DATE', - 'DATETIME' + 'DATETIME', ) .insensitive() .required(), @@ -67,10 +71,14 @@ const createTable = Joi.object({ .default('RESTRICT'), }), index: Joi.boolean(), - }) + }), ) .required(), }), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const getTableSchema = Joi.object({ @@ -83,6 +91,10 @@ const getTableSchema = Joi.object({ .required(), }), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const deleteTable = Joi.object({ @@ -95,6 +107,10 @@ const deleteTable = Joi.object({ .required(), }), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); module.exports = { diff --git a/src/server.js b/src/server.js index 027fb2a..ee47554 100755 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,6 @@ #!/usr/bin/env node const http = require('http'); -const express = require('express'); const app = require('./index'); const { wss } = require('./websocket'); @@ -9,7 +8,9 @@ const config = require('./config/index'); if (config.startWithStudio) { (async () => { - const { handler: soulStudioHandler } = await import('soul-studio/build/handler.js'); + const { handler: soulStudioHandler } = await import( + 'soul-studio/build/handler.js' + ); app.use('/studio', soulStudioHandler); })(); } diff --git a/src/services/authService.js b/src/services/authService.js new file mode 100644 index 0000000..32603f6 --- /dev/null +++ b/src/services/authService.js @@ -0,0 +1,78 @@ +const db = require('../db'); +const rowService = require('./rowService')(db); + +const { constantRoles, dbConstants } = require('../constants'); + +const { + USERS_TABLE, + ROLES_TABLE, + USERS_ROLES_TABLE, + ROLES_PERMISSIONS_TABLE, + tableFields, +} = dbConstants; + +module.exports = () => { + return { + getUsersByUsername({ username }) { + const users = rowService.get({ + tableName: USERS_TABLE, + whereString: `WHERE ${tableFields.USERNAME} =?`, + whereStringValues: [username], + }); + + return users; + }, + + getUsersById({ userId }) { + const users = rowService.get({ + tableName: USERS_TABLE, + whereString: `WHERE ${tableFields.ID}=?`, + whereStringValues: [userId], + }); + + return users; + }, + + getAllUsers() { + const users = rowService.get({ + tableName: USERS_TABLE, + whereString: '', + whereStringValues: [], + }); + + return users; + }, + + getPermissionByRoleIds({ roleIds }) { + const permissions = rowService.get({ + tableName: ROLES_PERMISSIONS_TABLE, + whereString: `WHERE ${tableFields.ROLE_ID} IN (${roleIds.map( + () => '?', + )})`, + whereStringValues: [...roleIds], + }); + + return permissions; + }, + + getUserRoleByUserId({ userId }) { + const userRoles = rowService.get({ + tableName: USERS_ROLES_TABLE, + whereString: `WHERE ${tableFields.USER_ID} =?`, + whereStringValues: [userId], + }); + + return userRoles; + }, + + getDefaultRole() { + const defaultRole = rowService.get({ + tableName: ROLES_TABLE, + whereString: `WHERE ${tableFields.ROLE_NAME}=?`, + whereStringValues: [constantRoles.DEFAULT_ROLE], + }); + + return defaultRole; + }, + }; +}; diff --git a/src/services/index.js b/src/services/index.js index 93e6d13..f87674a 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -2,5 +2,6 @@ const db = require('../db'); const rowService = require('./rowService')(db); const tableService = require('./tableService')(db); +const authService = require('./authService')(db); -module.exports = { rowService, tableService }; +module.exports = { rowService, tableService, authService }; diff --git a/src/services/rowService.js b/src/services/rowService.js index 4248ce1..177cdcf 100644 --- a/src/services/rowService.js +++ b/src/services/rowService.js @@ -1,19 +1,27 @@ +const { apiConstants } = require('../constants'); + module.exports = (db) => { return { get(data) { - const query = `SELECT ${data.schemaString} FROM ${data.tableName} ${data.extendString} ${data.whereString} ${data.orderString} LIMIT ? OFFSET ?`; + const query = `SELECT ${data.schemaString || '*'} FROM ${ + data.tableName + } ${data.extendString || ''} ${data.whereString || ''} ${ + data.orderString || '' + } LIMIT ? OFFSET ?`; + const statement = db.prepare(query); const result = statement.all( ...data.whereStringValues, - data.limit, - data.page + data.limit || apiConstants.DEFAULT_PAGE_LIMIT, + data.page || apiConstants.DEFAULT_PAGE_INDEX, ); + return result; }, getById(data) { const pks = data.pks.split(','); - const placeholders = pks.map((pk) => '?').join(','); + const placeholders = pks.map(() => '?').join(','); const query = `SELECT ${data.schemaString} FROM ${data.tableName} ${data.extendString} WHERE ${data.tableName}.${data.lookupField} in (${placeholders})`; const statement = db.prepare(query); const result = statement.all(...pks); @@ -29,7 +37,9 @@ module.exports = (db) => { save(data) { // wrap text values in quotes - const fieldsString = Object.keys(data.fields).join(', '); + const fieldsString = Object.keys(data.fields) + .map((field) => `'${field}'`) + .join(', '); // wrap text values in quotes const valuesString = Object.values(data.fields).map((value) => value); @@ -48,9 +58,28 @@ module.exports = (db) => { return result; }, + bulkWrite(data) { + const { tableName, fields } = data; + const fieldNames = Object.keys(fields[0]); + const valueSets = fields.map((row) => Object.values(row)); + + const placeholders = fieldNames.map(() => '?'); + const valuesString = valueSets + .map(() => `(${placeholders.join(',')})`) + .join(','); + + const query = `INSERT INTO ${tableName} (${fieldNames + .map((field) => `'${field}'`) + .join(', ')}) VALUES ${valuesString}`; + + const statement = db.prepare(query); + const result = statement.run(...valueSets.flat()); + return result; + }, + update(data) { const pks = data.pks.split(','); - const placeholders = pks.map((pk) => '?').join(','); + const placeholders = pks.map(() => '?').join(','); const query = `UPDATE ${data.tableName} SET ${data.fieldsString} WHERE ${data.lookupField} in (${placeholders})`; const statement = db.prepare(query); const result = statement.run(...pks); @@ -59,7 +88,7 @@ module.exports = (db) => { delete(data) { const pks = data.pks.split(','); - const placeholders = pks.map((pk) => '?').join(','); + const placeholders = pks.map(() => '?').join(','); const query = `DELETE FROM ${data.tableName} WHERE ${data.lookupField} in (${placeholders})`; const statement = db.prepare(query); const result = statement.run(...pks); diff --git a/src/services/tableService.js b/src/services/tableService.js index cfbe696..64cfbf7 100644 --- a/src/services/tableService.js +++ b/src/services/tableService.js @@ -1,5 +1,131 @@ module.exports = (db) => { return { - get() {}, + createTable(tableName, schema, options = {}) { + const { + autoAddCreatedAt = true, + autoAddUpdatedAt = true, + multipleUniqueConstraints, + } = options; + + let indices = []; + + let schemaString = schema + .map(({ name, type, notNull, unique, primaryKey, foreignKey }) => { + let column = `'${name}' '${type}'`; + + if (notNull) { + column += ' NOT NULL'; + } + if (unique) { + column += ' UNIQUE'; + } + if (primaryKey) { + column += ' PRIMARY KEY'; + } + if (foreignKey) { + column += ` REFERENCES ${foreignKey.table}(${foreignKey.column})`; + } + if (foreignKey && foreignKey.onDelete) { + column += ` ON DELETE ${foreignKey.onDelete}`; + } + if (foreignKey && foreignKey.onUpdate) { + column += ` ON UPDATE ${foreignKey.onUpdate}`; + } + + return column; + }) + .join(', '); + + // add id if primary key is not defined + if (!schema.find((field) => field.primaryKey)) { + schemaString = ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + ${schemaString} + `; + } + + // add created at and updated at + if (autoAddCreatedAt) { + schemaString = `${schemaString}, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP`; + } + + if (autoAddUpdatedAt) { + schemaString = `${schemaString}, updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP`; + } + + if (multipleUniqueConstraints) { + schemaString = `${schemaString}, CONSTRAINT ${ + multipleUniqueConstraints.name + } UNIQUE (${multipleUniqueConstraints.fields + .map((field) => field) + .join(' ,')})`; + } + + let indicesString = indices + .map((field) => { + return ` + CREATE INDEX ${tableName}_${field}_index + ON ${tableName} (${field}) + `; + }) + .join(';'); + + const query = `CREATE TABLE ${tableName} (${schemaString})`; + + try { + db.prepare(query).run(); + + if (indicesString) { + db.prepare(indicesString).run(); + } + + db.prepare(`PRAGMA table_info(${tableName})`).all(); + } catch (error) { + console.log(error); + } + }, + + listTables(options = {}) { + const { search, ordering, exclude } = options; + + let query = `SELECT name FROM sqlite_master WHERE type IN ('table', 'view')`; + + // if search is provided, search the tables + // e.g. search=users + if (search) { + query += ` AND name LIKE $searchQuery`; + } + + // if exclude is passed don't return the some tables + // e.g. exclude=['_users', '_roles'] + if (exclude) { + const excludeTables = exclude.map((field) => `'${field}'`).join(' ,'); + query += `AND name NOT IN (${excludeTables});`; + } + + // if ordering is provided, order the tables + // e.g. ordering=name (ascending) or ?_ordering=-name (descending) + if (ordering) { + query += ` ORDER BY $ordering`; + } + + try { + const tables = db.prepare(query).all({ + searchQuery: `%${search}%`, + ordering: `${ordering?.replace('-', '')} ${ + ordering?.startsWith('-') ? 'DESC' : 'ASC' + }`, + }); + return tables; + } catch (error) { + console.log(error); + } + }, + + checkTableExists(tableName) { + const query = `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`; + const result = db.prepare(query).get(); + return result; + }, }; }; diff --git a/src/swagger/index.js b/src/swagger/index.js index 4cee5e1..c65d585 100644 --- a/src/swagger/index.js +++ b/src/swagger/index.js @@ -31,6 +31,10 @@ const doc = { name: 'Rows', description: 'Rows endpoints', }, + { + name: 'Auth', + description: 'Auth endpoints', + }, ], securityDefinitions: {}, definitions: { @@ -122,6 +126,69 @@ const doc = { TransactionRequestBody: { $ref: '#/definitions/Transaction', }, + ObtainAccessTokenRequestBody: { + fields: { + username: '@john', + password: 'Ak22#cPM33@v*#', + }, + }, + + ObtainAccessTokenSuccessResponse: { + message: 'Success', + data: { + userId: 1, + }, + }, + + InvalidCredentialErrorResponse: { + message: 'Invalid username or password', + }, + + UserRegisterationRequestBody: { + fields: { + username: '@john', + password: 'Ak22#cPM33@v*#', + }, + }, + + WeakPasswordErrorResponse: { + message: 'This password is weak, please use another password', + }, + + UsernameTakenErrorResponse: { + message: 'This username is taken', + }, + + DefaultRoleNotCreatedErrorResponse: { + message: 'Please restart soul so a default role can be created', + }, + + UserNotFoundErrorResponse: { + message: 'User not found', + }, + + InvalidRefreshTokenErrorResponse: { + message: 'Invalid refresh token', + }, + + ChangePasswordRequestBody: { + fields: { + currentPassword: 'Ak22#cPM33@v*#', + newPassword: 'hKB33o@3245CD$', + }, + }, + + ChangePasswordSuccessResponse: { + message: 'Password updated successfully', + data: { id: 1, username: '@john' }, + }, + + RefreshAccessTokenSuccessResponse: { + message: 'Success', + data: { userId: 1 }, + }, + + InvalidPasswordErrorResponse: { message: 'Invalid password' }, }, }; diff --git a/src/swagger/swagger.json b/src/swagger/swagger.json index d3d26d2..2dad615 100644 --- a/src/swagger/swagger.json +++ b/src/swagger/swagger.json @@ -1,7 +1,7 @@ { "swagger": "2.0", "info": { - "version": "0.6.1", + "version": "0.7.0", "title": "Soul API", "description": "API Documentation for Soul, a SQLite REST and realtime server. " }, @@ -19,19 +19,16 @@ { "name": "Rows", "description": "Rows endpoints" + }, + { + "name": "Auth", + "description": "Auth endpoints" } ], - "schemes": [ - "http", - "https" - ], + "schemes": ["http", "https"], "securityDefinitions": {}, - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "consumes": ["application/json"], + "produces": ["application/json"], "paths": { "/health": { "get": { @@ -46,9 +43,7 @@ }, "/api/": { "get": { - "tags": [ - "Root" - ], + "tags": ["Root"], "summary": "Timestamp", "description": "Endpoint to return server timestamp", "parameters": [], @@ -61,9 +56,7 @@ }, "/api/transaction": { "post": { - "tags": [ - "Root" - ], + "tags": ["Root"], "summary": "Transaction", "description": "Endpoint to run any transaction, e.g. [{ \"query\": \"\" }, { \"statement\": \"\", \"values\": {} }, { \"query\": \"\" }]", "parameters": [ @@ -88,9 +81,7 @@ }, "/api/tables/": { "get": { - "tags": [ - "Tables" - ], + "tags": ["Tables"], "summary": "List Tables", "description": "Endpoint to list all tables", "parameters": [ @@ -115,13 +106,17 @@ }, "400": { "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" } } }, "post": { - "tags": [ - "Tables" - ], + "tags": ["Tables"], "summary": "Create Table", "description": "Endpoint to create a table", "parameters": [ @@ -146,15 +141,22 @@ "schema": { "$ref": "#/definitions/CreateTableErrorResponse" } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "409": { + "description": "Conflict" } } } }, "/api/tables/{name}": { "get": { - "tags": [ - "Tables" - ], + "tags": ["Tables"], "summary": "Get Table Schema", "description": "Endpoint to get the schema of a table", "parameters": [ @@ -172,13 +174,17 @@ }, "400": { "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" } } }, "delete": { - "tags": [ - "Tables" - ], + "tags": ["Tables"], "summary": "Delete Table", "description": "Endpoint to delete a table", "parameters": [ @@ -196,15 +202,19 @@ }, "400": { "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" } } } }, "/api/tables/{name}/rows": { "get": { - "tags": [ - "Rows" - ], + "tags": ["Rows"], "summary": "List Rows", "description": "Endpoint to list rows of a table.", "parameters": [ @@ -260,18 +270,19 @@ } ], "responses": { - "200": { - "description": "OK" - }, "400": { "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" } } }, "post": { - "tags": [ - "Rows" - ], + "tags": ["Rows"], "summary": "Insert Row", "description": "Insert a new row in a table", "parameters": [ @@ -303,15 +314,19 @@ "schema": { "$ref": "#/definitions/InsertRowErrorResponse" } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" } } } }, "/api/tables/{name}/rows/{pks}": { "get": { - "tags": [ - "Rows" - ], + "tags": ["Rows"], "summary": "Retrieve Row", "description": "Retrieve a row by primary key", "parameters": [ @@ -352,21 +367,22 @@ } ], "responses": { - "200": { - "description": "OK" - }, "400": { "description": "Bad Request" }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, "404": { "description": "Not Found" } } }, "put": { - "tags": [ - "Rows" - ], + "tags": ["Rows"], "summary": "Update Row", "description": "Update a row by primary key", "parameters": [ @@ -426,13 +442,17 @@ }, "400": { "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" } } }, "delete": { - "tags": [ - "Rows" - ], + "tags": ["Rows"], "summary": "Delete Row", "description": "Delete a row by primary key", "parameters": [ @@ -465,11 +485,56 @@ "400": { "description": "Bad Request" }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, "404": { "description": "Not Found" } } } + }, + "/api/auth/token/obtain": { + "post": { + "description": "", + "parameters": [], + "responses": { + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/auth/token/refresh": { + "get": { + "description": "", + "parameters": [], + "responses": { + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/auth/change-password": { + "put": { + "description": "", + "parameters": [], + "responses": { + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + } + } } }, "definitions": { @@ -718,11 +783,7 @@ "properties": { "pks": { "type": "array", - "example": [ - 1, - 2, - 3 - ], + "example": [1, 2, 3], "items": { "type": "number" } @@ -740,11 +801,7 @@ "properties": { "pks": { "type": "array", - "example": [ - 1, - 2, - 3 - ], + "example": [1, 2, 3], "items": { "type": "number" } @@ -753,6 +810,181 @@ }, "TransactionRequestBody": { "$ref": "#/definitions/Transaction" + }, + "ObtainAccessTokenRequestBody": { + "type": "object", + "properties": { + "fields": { + "type": "object", + "properties": { + "username": { + "type": "string", + "example": "@john" + }, + "password": { + "type": "string", + "example": "Ak22#cPM33@v*#" + } + } + } + } + }, + "ObtainAccessTokenSuccessResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Success" + }, + "data": { + "type": "object", + "properties": { + "userId": { + "type": "number", + "example": 1 + } + } + } + } + }, + "InvalidCredentialErrorResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Invalid username or password" + } + } + }, + "UserRegisterationRequestBody": { + "type": "object", + "properties": { + "fields": { + "type": "object", + "properties": { + "username": { + "type": "string", + "example": "@john" + }, + "password": { + "type": "string", + "example": "Ak22#cPM33@v*#" + } + } + } + } + }, + "WeakPasswordErrorResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "This password is weak, please use another password" + } + } + }, + "UsernameTakenErrorResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "This username is taken" + } + } + }, + "DefaultRoleNotCreatedErrorResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Please restart soul so a default role can be created" + } + } + }, + "UserNotFoundErrorResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "User not found" + } + } + }, + "InvalidRefreshTokenErrorResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Invalid refresh token" + } + } + }, + "ChangePasswordRequestBody": { + "type": "object", + "properties": { + "fields": { + "type": "object", + "properties": { + "currentPassword": { + "type": "string", + "example": "Ak22#cPM33@v*#" + }, + "newPassword": { + "type": "string", + "example": "hKB33o@3245CD$" + } + } + } + } + }, + "ChangePasswordSuccessResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Password updated successfully" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "username": { + "type": "string", + "example": "@john" + } + } + } + } + }, + "RefreshAccessTokenSuccessResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Success" + }, + "data": { + "type": "object", + "properties": { + "userId": { + "type": "number", + "example": 1 + } + } + } + } + }, + "InvalidPasswordErrorResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Invalid password" + } + } } } -} \ No newline at end of file +} diff --git a/src/tests/index.js b/src/tests/index.js index 6ce5245..c1e4dec 100644 --- a/src/tests/index.js +++ b/src/tests/index.js @@ -1,6 +1,5 @@ const fs = require('fs'); const { unlink } = require('fs/promises'); - const db = require('../db/index'); const { testNames } = require('./testData'); @@ -18,7 +17,7 @@ const dropTestDatabase = async (path = 'test.db') => { if (fs.existsSync(path + '-wal')) { try { - await Promise.allSettled(unlink(path + '-wal'), unlink(path + '-shm')); + await Promise.allSettled([unlink(path + '-wal'), unlink(path + '-shm')]); } catch (error) { console.error('there was an error:', error); } @@ -27,13 +26,13 @@ const dropTestDatabase = async (path = 'test.db') => { const createTestTable = (table = 'users') => { db.prepare( - `CREATE TABLE ${table} (id INTEGER PRIMARY KEY, firstName TEXT, lastName TEXT, email TEXT, username TEXT, createdAt TEXT)` + `CREATE TABLE ${table} (id INTEGER PRIMARY KEY, firstName TEXT, lastName TEXT, email TEXT, username TEXT, createdAt TEXT)`, ).run(); }; const insertIntoTestTable = (table = 'users') => { const statement = db.prepare( - `INSERT INTO ${table} (firstName, lastName, createdAt) VALUES (?, ?, ?)` + `INSERT INTO ${table} (firstName, lastName, createdAt) VALUES (?, ?, ?)`, ); for (const user of testNames) { @@ -45,5 +44,5 @@ module.exports = { dropTestTable, dropTestDatabase, createTestTable, - insertIntoTestTable + insertIntoTestTable, }; diff --git a/src/tests/testData.js b/src/tests/testData.js index bb0408c..21c1e72 100644 --- a/src/tests/testData.js +++ b/src/tests/testData.js @@ -6,37 +6,37 @@ const testNames = [ { firstName: 'Olivia', lastName: 'William', - createdAt: '2012-01-08 00:00:00' + createdAt: '2012-01-08 00:00:00', }, { firstName: 'William', lastName: 'Kim', createdAt: '2013-01-08 00:00:00' }, { firstName: 'Sophia', lastName: 'Singh', createdAt: '2013-02-08 00:00:00' }, { firstName: 'James', lastName: 'Rodriguez', - createdAt: '2013-03-08 00:00:00' + createdAt: '2013-03-08 00:00:00', }, { firstName: 'Ava', lastName: 'Patel', createdAt: '2013-01-04 00:00:00' }, { firstName: 'Benjamin', lastName: 'Garcia', - createdAt: '2015-01-08 00:00:00' + createdAt: '2015-01-08 00:00:00', }, { firstName: 'Isabella', lastName: 'Nguyen', - createdAt: '2014-01-08 00:00:00' + createdAt: '2014-01-08 00:00:00', }, { firstName: 'Ethan', lastName: 'Lee', createdAt: '2016-01-08 00:00:00' }, { firstName: 'Mia', lastName: 'Wilson', createdAt: '2017-01-08 00:00:00' }, { firstName: 'Alexander', lastName: 'William', - createdAt: '2018-01-08 00:00:00' + createdAt: '2018-01-08 00:00:00', }, { firstName: 'Charlotte', lastName: 'Hernandez', - createdAt: '2019-01-08 00:00:00' + createdAt: '2019-01-08 00:00:00', }, { firstName: 'Liam', lastName: 'Gonzalez', createdAt: '2020-01-08 00:00:00' }, { firstName: 'Emma', lastName: 'Gomez', createdAt: '2021-01-08 00:00:00' }, @@ -46,17 +46,30 @@ const testNames = [ { firstName: 'Abigail', lastName: 'Williams', - createdAt: '2023-02-10 00:00:00' + createdAt: '2023-02-10 00:00:00', }, { firstName: 'Elijah', lastName: 'Hall', createdAt: '2023-04-02 00:00:00' }, { firstName: 'Mila', lastName: 'Flores', createdAt: '2023-05-13 00:00:00' }, { firstName: 'Evelyn', lastName: 'Morales', - createdAt: '2023-06-05 00:00:00' + createdAt: '2023-06-05 00:00:00', }, { firstName: 'Logan', lastName: 'Collins', createdAt: '2023-06-07 00:00:00' }, - { firstName: null, lastName: 'Flores', createdAt: '2023-06-09 00:00:00' } + { firstName: null, lastName: 'Flores', createdAt: '2023-06-09 00:00:00' }, ]; -module.exports = { testNames }; +const testData = { + strongPassword: 'HeK34#C44DMJ', + strongPassword2: 'Mk22#c9@Cv!K', + weakPassword: '12345678', + invalidUsername: 'invalid_username', + invalidPassword: 'invalid_password', + users: { + user1: { username: 'Jane' }, + user2: { username: 'Mike' }, + user3: { username: 'John' }, + }, +}; + +module.exports = { testNames, testData }; diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..28f9faa --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,78 @@ +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); + +const { passwordStrength } = require('check-password-strength'); + +const hashPassword = async (password, saltRounds) => { + const salt = await bcrypt.genSalt(saltRounds); + const hashedPassword = await bcrypt.hash(password, saltRounds); + return { hashedPassword, salt }; +}; + +const comparePasswords = async (plainPassword, hashedPassword) => { + const isMatch = await bcrypt.compare(plainPassword, hashedPassword); + return isMatch; +}; + +const checkPasswordStrength = (password) => { + const value = passwordStrength(password).value; + return value; +}; + +const generateToken = async (payload, secret, expiresIn) => { + return jwt.sign(payload, secret, { expiresIn }); +}; + +const decodeToken = async (token, secret) => { + try { + const decoded = jwt.verify(token, secret); + return decoded; + } catch (error) { + throw new Error('Invalid token'); + } +}; + +const toBoolean = (value) => { + if (typeof value === 'boolean') { + return value; + } + + if (typeof value === 'string') { + const lowerCaseValue = value.toLowerCase(); + if (lowerCaseValue === 'true') { + return true; + } else if (lowerCaseValue === 'false') { + return false; + } + } + + if (typeof value === 'number') { + if (value === 1) { + return true; + } else if (value === 0) { + return false; + } + } + + throw new Error('Invalid value. Cannot convert to boolean.'); +}; + +const removeFields = async (rows, fields) => { + const newPayload = rows.map((row) => { + fields.map((field) => { + delete row[field]; + }); + }); + + return newPayload; +}; + +module.exports = { + hashPassword, + comparePasswords, + checkPasswordStrength, + generateToken, + decodeToken, + toBoolean, + removeFields, +};