Merge pull request #152 from thevahidal/user_registration_feature
User registration feature
This commit is contained in:
10
.env.sample
10
.env.sample
@@ -9,6 +9,16 @@ RATE_LIMIT_ENABLED=false
|
||||
RATE_LIMIT_WINDOW_MS=1000
|
||||
RATE_LIMIT_MAX_REQUESTS=10
|
||||
|
||||
JWT_SECRET=ABCD23DCAA
|
||||
JWT_EXPIRATION_TIME=10H
|
||||
|
||||
INITIAL_USER_USERNAME=<your_username>
|
||||
INITIAL_USER_PASSWORD=<your_password>
|
||||
|
||||
DB=foobar.db
|
||||
|
||||
START_WITH_STUDIO=false
|
||||
|
||||
TOKEN_SECRET=ABCD23DCAA
|
||||
ACCESS_TOKEN_EXPIRATION_TIME=10H
|
||||
REFRESH_TOKEN_EXPIRATION_TIME=2D
|
||||
|
||||
52
README.md
52
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).
|
||||
|
||||
@@ -33,6 +35,14 @@ Options:
|
||||
-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]
|
||||
|
||||
-ats, --accesstokensecret Access Token Secret [string]
|
||||
-atet, --accesstokenexpirationtime Access Token Expiration Time [string]
|
||||
-rts, --refreshtokensecret Refresh Token Secret [string]
|
||||
-rtet, --refreshtokenexpirationtime Refresh Token Expiration Time [string]
|
||||
-S, --studio Start Soul Studio in parallel
|
||||
--help Show help
|
||||
|
||||
@@ -46,7 +56,39 @@ curl http://localhost:8000/api/tables
|
||||
|
||||
It should return a list of the tables inside `sqlite.db` database.
|
||||
|
||||
### Updating superusers
|
||||
### 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 <your_jwt_secret_value> -atet=4H -rtet=3D -iuu=john -iup=<your_password>
|
||||
|
||||
```
|
||||
|
||||
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 <your_jwt_secret_value> 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:
|
||||
|
||||
@@ -96,6 +138,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.
|
||||
|
||||
151
package-lock.json
generated
151
package-lock.json
generated
@@ -12,12 +12,15 @@
|
||||
"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",
|
||||
@@ -2298,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",
|
||||
@@ -2380,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",
|
||||
@@ -2571,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",
|
||||
@@ -2764,6 +2797,14 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -5876,6 +5917,81 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||
"dependencies": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken/node_modules/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/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/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": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken/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/jwa": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -5944,12 +6060,47 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"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",
|
||||
|
||||
33
src/cli.js
33
src/cli.js
@@ -53,6 +53,39 @@ if (process.env.NO_CLI !== 'true') {
|
||||
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',
|
||||
|
||||
@@ -29,6 +29,13 @@ const envVarsSchema = Joi.object()
|
||||
EXTENSIONS: Joi.string().default(null),
|
||||
|
||||
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();
|
||||
|
||||
@@ -60,6 +67,26 @@ 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);
|
||||
@@ -87,6 +114,16 @@ module.exports = {
|
||||
},
|
||||
|
||||
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,
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
module.exports = {
|
||||
defaultRoutes: ['_users', '_roles', '_roles_permissions', '_users_roles'],
|
||||
baseTableUrl: '/api/tables',
|
||||
fields: {
|
||||
_users: {
|
||||
SALT: 'salt',
|
||||
IS_SUPERUSER: 'is_superuser',
|
||||
HASHED_PASSWORD: 'hashed_password',
|
||||
},
|
||||
},
|
||||
DEFAULT_PAGE_LIMIT: 10,
|
||||
DEFAULT_PAGE_INDEX: 0,
|
||||
PASSWORD: {
|
||||
TOO_WEAK: 'Too weak',
|
||||
WEAK: 'Weak',
|
||||
},
|
||||
};
|
||||
|
||||
6
src/constants/httpVerbs.js
Normal file
6
src/constants/httpVerbs.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
POST: 'CREATE',
|
||||
GET: 'READ',
|
||||
PUT: 'UPDATE',
|
||||
DELETE: 'DELETE',
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
const dbTables = require('./dbTables');
|
||||
const constantRoles = require('./roles');
|
||||
const dbConstants = require('./tables');
|
||||
const apiConstants = require('./api');
|
||||
const constantRoles = require('./roles');
|
||||
|
||||
module.exports = { dbTables, constantRoles, apiConstants };
|
||||
module.exports = { dbConstants, apiConstants, constantRoles };
|
||||
|
||||
13
src/constants/tables.js
Normal file
13
src/constants/tables.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
reservedTableNames: [
|
||||
'_users',
|
||||
'_roles',
|
||||
'_roles_permissions',
|
||||
'_users_roles',
|
||||
],
|
||||
|
||||
USER_TABLE: '_users',
|
||||
ROLE_TABLE: '_roles',
|
||||
USER_ROLES_TABLE: '_users_roles',
|
||||
ROLE_PERMISSIONS_TABLE: '_roles_permissions',
|
||||
};
|
||||
@@ -1,38 +1,60 @@
|
||||
const { tableService } = require('../services');
|
||||
const { rowService } = require('../services');
|
||||
const { dbTables, constantRoles } = require('../constants');
|
||||
const { hashPassword, checkPasswordStrength } = require('../utils');
|
||||
const { tableService, rowService } = require('../services');
|
||||
const { constantRoles, apiConstants, dbConstants } = require('../constants');
|
||||
const schema = require('../db/schema');
|
||||
const config = require('../config');
|
||||
const {
|
||||
hashPassword,
|
||||
checkPasswordStrength,
|
||||
comparePasswords,
|
||||
generateToken,
|
||||
decodeToken,
|
||||
toBoolean,
|
||||
} = require('../utils');
|
||||
|
||||
const { USER_TABLE, ROLE_TABLE, USERS_ROLES_TABLE, ROLE_PERMISSIONS_TABLE } =
|
||||
dbConstants;
|
||||
|
||||
const createDefaultTables = async () => {
|
||||
let roleId;
|
||||
|
||||
// check if the default tables are already created
|
||||
const roleTable = tableService.checkTableExists('_roles');
|
||||
const usersTable = tableService.checkTableExists('_users');
|
||||
const rolesPermissionTable =
|
||||
tableService.checkTableExists('_roles_permissions');
|
||||
const usersRolesTable = tableService.checkTableExists('_users_roles');
|
||||
const roleTable = tableService.checkTableExists(ROLE_TABLE);
|
||||
const usersTable = tableService.checkTableExists(USER_TABLE);
|
||||
const rolesPermissionTable = tableService.checkTableExists(
|
||||
ROLE_PERMISSIONS_TABLE,
|
||||
);
|
||||
const usersRolesTable = tableService.checkTableExists(USERS_ROLES_TABLE);
|
||||
|
||||
// create _users table
|
||||
if (!usersTable) {
|
||||
// create the _users table
|
||||
tableService.createTable('_users', dbTables.userSchema);
|
||||
tableService.createTable(USER_TABLE, schema.userSchema);
|
||||
}
|
||||
|
||||
// create _users_roles table
|
||||
if (!usersRolesTable) {
|
||||
// create the _users_roles table
|
||||
tableService.createTable('_users_roles', dbTables.usersRoleSchema);
|
||||
tableService.createTable(
|
||||
USERS_ROLES_TABLE,
|
||||
|
||||
schema.usersRoleSchema,
|
||||
{
|
||||
multipleUniqueConstraints: {
|
||||
name: 'unique_users_role',
|
||||
fields: ['user_id', 'role_id'],
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// create _roles table
|
||||
if (!roleTable) {
|
||||
// create the _role table
|
||||
tableService.createTable('_roles', dbTables.roleSchema);
|
||||
tableService.createTable(ROLE_TABLE, schema.roleSchema);
|
||||
|
||||
// create a default role in the _roles table
|
||||
const role = rowService.save({
|
||||
tableName: '_roles',
|
||||
tableName: ROLE_TABLE,
|
||||
fields: { name: constantRoles.DEFAULT_ROLE },
|
||||
});
|
||||
roleId = role.lastInsertRowid;
|
||||
@@ -42,8 +64,8 @@ const createDefaultTables = async () => {
|
||||
if (!rolesPermissionTable && roleId) {
|
||||
// create the _roles_permissions table
|
||||
tableService.createTable(
|
||||
'_roles_permissions',
|
||||
dbTables.rolePermissionSchema,
|
||||
ROLE_PERMISSIONS_TABLE,
|
||||
schema.rolePermissionSchema,
|
||||
{
|
||||
multipleUniqueConstraints: {
|
||||
name: 'unique_role_table',
|
||||
@@ -70,7 +92,7 @@ const createDefaultTables = async () => {
|
||||
|
||||
// store the permissions in the db
|
||||
rowService.bulkWrite({
|
||||
tableName: '_roles_permissions',
|
||||
tableName: ROLE_PERMISSIONS_TABLE,
|
||||
fields: permissions,
|
||||
});
|
||||
}
|
||||
@@ -84,7 +106,7 @@ const updateSuperuser = async (fields) => {
|
||||
try {
|
||||
// find the user by using the id field
|
||||
const users = rowService.get({
|
||||
tableName: '_users',
|
||||
tableName: USER_TABLE,
|
||||
whereString: 'WHERE id=?',
|
||||
whereStringValues: [id],
|
||||
});
|
||||
@@ -103,7 +125,11 @@ const updateSuperuser = async (fields) => {
|
||||
// if the password is sent from the CLI, update it
|
||||
if (password) {
|
||||
// check if the password is weak
|
||||
if (['Too weak', 'Weak'].includes(checkPasswordStrength(password))) {
|
||||
if (
|
||||
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
|
||||
checkPasswordStrength(password),
|
||||
)
|
||||
) {
|
||||
console.log('Your password should be at least 8 charachters long');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -120,7 +146,7 @@ const updateSuperuser = async (fields) => {
|
||||
|
||||
// update the user
|
||||
rowService.update({
|
||||
tableName: '_users',
|
||||
tableName: USER_TABLE,
|
||||
lookupField: `id`,
|
||||
fieldsString,
|
||||
pks: `${id}`,
|
||||
@@ -135,4 +161,551 @@ const updateSuperuser = async (fields) => {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { createDefaultTables, updateSuperuser };
|
||||
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: 'username is required' });
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
return res.status(400).send({ message: 'password is required' });
|
||||
}
|
||||
|
||||
// check if the username is taken
|
||||
let user = rowService.get({
|
||||
tableName: USER_TABLE,
|
||||
whereString: 'WHERE username=?',
|
||||
whereStringValues: [username],
|
||||
});
|
||||
|
||||
if (user.length > 0) {
|
||||
return res.status(409).send({ message: 'This username is taken' });
|
||||
|
||||
/*
|
||||
#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: 'This password is weak, please use another password',
|
||||
});
|
||||
|
||||
/*
|
||||
#swagger.responses[400] = {
|
||||
description: 'Weak password error',
|
||||
schema: {
|
||||
$ref: '#/definitions/WeakPasswordErrorResponse'
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// hash the password
|
||||
const { salt, hashedPassword } = await hashPassword(password, 10);
|
||||
|
||||
// create the user
|
||||
const newUser = rowService.save({
|
||||
tableName: USER_TABLE,
|
||||
fields: {
|
||||
username,
|
||||
salt,
|
||||
hashed_password: hashedPassword,
|
||||
is_superuser: 'false',
|
||||
},
|
||||
});
|
||||
|
||||
// find the default role from the DB
|
||||
let defaultRole = rowService.get({
|
||||
tableName: ROLE_TABLE,
|
||||
whereString: 'WHERE name=?',
|
||||
whereStringValues: [constantRoles.DEFAULT_ROLE],
|
||||
});
|
||||
|
||||
if (defaultRole.length <= 0) {
|
||||
return res.status(500).send({
|
||||
message: 'Please restart soul so a default role can be created',
|
||||
});
|
||||
/*
|
||||
#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: 'Row Inserted' });
|
||||
|
||||
/*
|
||||
#swagger.responses[201] = {
|
||||
description: 'Row inserted',
|
||||
schema: {
|
||||
$ref: '#/definitions/InsertRowSuccessResponse'
|
||||
}
|
||||
}
|
||||
*/
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).send({ message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
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 = rowService.get({
|
||||
tableName: USER_TABLE,
|
||||
whereString: 'WHERE username=?',
|
||||
whereStringValues: [username],
|
||||
});
|
||||
|
||||
if (users.length <= 0) {
|
||||
return res.status(401).send({ message: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
// 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: 'Invalid username or password' });
|
||||
/*
|
||||
#swagger.responses[401] = {
|
||||
description: 'Invalid username or password error',
|
||||
schema: {
|
||||
$ref: '#/definitions/InvalidCredentialErrorResponse'
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
let userRoles, permissions, roleIds;
|
||||
|
||||
// if the user is not a superuser get the role and its permission from the DB
|
||||
if (!toBoolean(user.is_superuser)) {
|
||||
userRoles = rowService.get({
|
||||
tableName: USERS_ROLES_TABLE,
|
||||
whereString: 'WHERE user_id=?',
|
||||
whereStringValues: [user.id],
|
||||
});
|
||||
|
||||
if (userRoles <= 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ message: 'Role not found for this user' });
|
||||
}
|
||||
|
||||
roleIds = userRoles.map((role) => role.role_id);
|
||||
|
||||
// get the permission of the role
|
||||
permissions = rowService.get({
|
||||
tableName: ROLE_PERMISSIONS_TABLE,
|
||||
whereString: `WHERE role_id IN (${roleIds.map(() => '?')})`,
|
||||
whereStringValues: [...roleIds],
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
username: user.username,
|
||||
userId: user.id,
|
||||
isSuperuser: user.is_superuser,
|
||||
roleIds,
|
||||
permissions,
|
||||
};
|
||||
|
||||
// generate an access token
|
||||
const accessToken = await generateToken(
|
||||
{ subject: 'accessToken', ...payload },
|
||||
config.tokenSecret,
|
||||
config.accessTokenExpirationTime,
|
||||
);
|
||||
|
||||
// generate a refresh token
|
||||
const refreshToken = await generateToken(
|
||||
{ subject: 'refreshToken', ...payload },
|
||||
config.tokenSecret,
|
||||
config.refreshTokenExpirationTime,
|
||||
);
|
||||
|
||||
// set the token in the cookie
|
||||
let cookieOptions = { httpOnly: true, secure: false, Path: '/' };
|
||||
res.cookie('accessToken', accessToken, cookieOptions);
|
||||
res.cookie('refreshToken', refreshToken, cookieOptions);
|
||||
|
||||
res.status(201).send({ message: '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: error.message,
|
||||
error: 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 = rowService.get({
|
||||
tableName: USER_TABLE,
|
||||
whereString: 'WHERE id=?',
|
||||
whereStringValues: [payload.userId],
|
||||
});
|
||||
|
||||
if (users.length <= 0) {
|
||||
return res
|
||||
.status(401)
|
||||
.send({ message: `User with userId = ${payload.userId} not found` });
|
||||
|
||||
/*
|
||||
#swagger.responses[401] = {
|
||||
description: 'User not found error',
|
||||
schema: {
|
||||
$ref: '#/definitions/UserNotFoundErrorResponse'
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
let userRoles, 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)) {
|
||||
userRoles = rowService.get({
|
||||
tableName: USERS_ROLES_TABLE,
|
||||
whereString: 'WHERE user_id=?',
|
||||
whereStringValues: [user.id],
|
||||
});
|
||||
|
||||
roleIds = userRoles.map((role) => role.role_id);
|
||||
|
||||
// get the permission of the role
|
||||
permissions = rowService.get({
|
||||
tableName: ROLE_PERMISSIONS_TABLE,
|
||||
whereString: `WHERE role_id IN (${roleIds.map(() => '?')})`,
|
||||
whereStringValues: [...roleIds],
|
||||
});
|
||||
}
|
||||
|
||||
const newPayload = {
|
||||
username: user.username,
|
||||
userId: user.id,
|
||||
isSuperuser: user.is_superuser,
|
||||
roleIds,
|
||||
permissions,
|
||||
};
|
||||
|
||||
// generate an access token
|
||||
const accessToken = await generateToken(
|
||||
{ subject: 'accessToken', ...newPayload },
|
||||
config.tokenSecret,
|
||||
config.accessTokenExpirationTime,
|
||||
);
|
||||
|
||||
// generate a refresh token
|
||||
const refreshToken = await generateToken(
|
||||
{ subject: 'refreshToken', ...newPayload },
|
||||
config.tokenSecret,
|
||||
config.refreshTokenExpirationTime,
|
||||
);
|
||||
|
||||
// set the token in the cookie
|
||||
let cookieOptions = { httpOnly: true, secure: false, Path: '/' };
|
||||
res.cookie('accessToken', accessToken, cookieOptions);
|
||||
res.cookie('refreshToken', refreshToken, cookieOptions);
|
||||
|
||||
res.status(200).send({ message: 'Success', data: { userId: user.id } });
|
||||
|
||||
/*
|
||||
#swagger.responses[200] = {
|
||||
description: 'Access token refreshed',
|
||||
schema: {
|
||||
$ref: '#/definitions/RefreshAccessTokenSuccessResponse'
|
||||
}
|
||||
}
|
||||
*/
|
||||
} catch (error) {
|
||||
res.status(403).send({ message: 'Invalid refresh token' });
|
||||
/*
|
||||
#swagger.responses[401] = {
|
||||
description: 'Invalid refresh token error',
|
||||
schema: {
|
||||
$ref: '#/definitions/InvalidRefreshTokenErrorResponse'
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
};
|
||||
|
||||
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 = rowService.get({
|
||||
tableName: USER_TABLE,
|
||||
whereString: 'WHERE id=?',
|
||||
whereStringValues: [userInfo.userId],
|
||||
});
|
||||
|
||||
if (users.length <= 0) {
|
||||
return res.status(401).send({ message: 'User not found' });
|
||||
}
|
||||
|
||||
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: 'Invalid current password' });
|
||||
/*
|
||||
#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: 'This password is weak, please use another password',
|
||||
});
|
||||
|
||||
/*
|
||||
#swagger.responses[400] = {
|
||||
description: 'Weak password error',
|
||||
schema: {
|
||||
$ref: '#/definitions/WeakPasswordErrorResponse'
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// hash the password
|
||||
const { salt, hashedPassword } = await hashPassword(newPassword, 10);
|
||||
|
||||
user.salt = salt;
|
||||
user.hashed_password = hashedPassword;
|
||||
|
||||
// update the user
|
||||
rowService.update({
|
||||
tableName: USER_TABLE,
|
||||
lookupField: `id`,
|
||||
fieldsString: `hashed_password = '${hashedPassword}', salt = '${salt}'`,
|
||||
pks: `${user.id}`,
|
||||
});
|
||||
|
||||
res.status(200).send({
|
||||
message: 'Password updated successfully',
|
||||
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: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
const createInitialUser = async () => {
|
||||
// extract some fields from the environment variables or from the CLI
|
||||
const { initialUserUsername: username, initialUserPassword: password } =
|
||||
config;
|
||||
|
||||
try {
|
||||
// check if there is are users in the DB
|
||||
const users = rowService.get({
|
||||
tableName: USER_TABLE,
|
||||
whereString: '',
|
||||
whereStringValues: [],
|
||||
});
|
||||
|
||||
if (users.length <= 0) {
|
||||
// check if initial users username is passed from the env or CLI
|
||||
if (!username) {
|
||||
console.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',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// check if initial users password is passed from the env or CLI
|
||||
if (!password) {
|
||||
console.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',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// check if the usernmae is taken
|
||||
const users = rowService.get({
|
||||
tableName: USER_TABLE,
|
||||
whereString: 'WHERE username=?',
|
||||
whereStringValues: [username],
|
||||
});
|
||||
|
||||
if (users.length > 0) {
|
||||
console.error(
|
||||
'Error: The username you passed for the initial user is already taken, please use another username',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// check if the password is strong
|
||||
if (
|
||||
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
|
||||
checkPasswordStrength(password),
|
||||
)
|
||||
) {
|
||||
console.error(
|
||||
'Error: The password you passed for the initial user is weak, please use another password',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// hash the password
|
||||
const { hashedPassword, salt } = await hashPassword(password, 10);
|
||||
|
||||
// create the initial user
|
||||
rowService.save({
|
||||
tableName: USER_TABLE,
|
||||
fields: {
|
||||
username,
|
||||
hashed_password: hashedPassword,
|
||||
salt,
|
||||
is_superuser: 'false',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Initial user created');
|
||||
} else {
|
||||
console.log('Initial user is already created');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const isUsernameTaken = (username) => {
|
||||
let user = rowService.get({
|
||||
tableName: USER_TABLE,
|
||||
whereString: 'WHERE username=?',
|
||||
whereStringValues: [username],
|
||||
});
|
||||
|
||||
return user.length > 0;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createDefaultTables,
|
||||
updateSuperuser,
|
||||
registerUser,
|
||||
obtainAccessToken,
|
||||
refreshAccessToken,
|
||||
changePassword,
|
||||
createInitialUser,
|
||||
isUsernameTaken,
|
||||
};
|
||||
|
||||
313
src/controllers/auth.test.js
Normal file
313
src/controllers/auth.test.js
Normal file
@@ -0,0 +1,313 @@
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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',
|
||||
);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('PUT /tables/_users/rows/:id should throw a 409 error if the username is taken', 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.user1.username, //A user with user1.username is already created in the first test suite
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(409);
|
||||
expect(res.body.message).toEqual('This username is already taken');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Refresh Access Token Endpoint', () => {
|
||||
it('GET /auth/token/refresh should refresh the access and refresh tokens', async () => {
|
||||
const accessToken = await generateToken(
|
||||
{ username: 'John', userId: 1, isSuperuser: true },
|
||||
config.tokenSecret,
|
||||
'1H',
|
||||
);
|
||||
|
||||
const refreshToken = await generateToken(
|
||||
{ username: 'John', userId: 1, isSuperuser: true },
|
||||
config.tokenSecret,
|
||||
'1H',
|
||||
);
|
||||
|
||||
const res = await requestWithSupertest
|
||||
.get('/api/auth/token/refresh')
|
||||
.set('Cookie', [
|
||||
`accessToken=${accessToken}`,
|
||||
`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');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -98,7 +98,7 @@ module.exports = {
|
||||
type: 'NUMERIC',
|
||||
primaryKey: false,
|
||||
notNull: true,
|
||||
unique: true,
|
||||
unique: false,
|
||||
foreignKey: { table: '_users', column: 'id' },
|
||||
},
|
||||
|
||||
16
src/index.js
16
src/index.js
@@ -7,15 +7,23 @@ 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 } = require('./controllers/auth');
|
||||
const {
|
||||
createDefaultTables,
|
||||
createInitialUser,
|
||||
} = require('./controllers/auth');
|
||||
|
||||
const { runCLICommands } = require('./commands');
|
||||
|
||||
const app = express();
|
||||
@@ -24,6 +32,7 @@ app.get('/health', (req, res) => {
|
||||
});
|
||||
|
||||
app.use(bodyParser.json());
|
||||
app.use(cookieParser());
|
||||
|
||||
// Activate wal mode
|
||||
db.exec('PRAGMA journal_mode = WAL');
|
||||
@@ -70,9 +79,10 @@ if (config.rateLimit.enabled) {
|
||||
app.use(limiter);
|
||||
}
|
||||
|
||||
//If Auth mode is activated then create auth tables in the DB
|
||||
//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.',
|
||||
@@ -87,6 +97,8 @@ app.use('/api', rootRoutes);
|
||||
app.use('/api/tables', tablesRoutes);
|
||||
app.use('/api/tables', rowsRoutes);
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
|
||||
setupExtensions(app, db);
|
||||
|
||||
module.exports = app;
|
||||
|
||||
81
src/middlewares/api.js
Normal file
81
src/middlewares/api.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const config = require('../config');
|
||||
const { registerUser, isUsernameTaken } = require('../controllers/auth');
|
||||
const { apiConstants, dbConstants } = require('../constants/');
|
||||
const { removeFields } = require('../utils');
|
||||
|
||||
const { SALT, HASHED_PASSWORD, IS_SUPERUSER } = apiConstants.fields._users;
|
||||
const { reservedTableNames } = dbConstants;
|
||||
|
||||
const processRowRequest = async (req, res, next) => {
|
||||
const resource = req.params.name;
|
||||
const { method, body } = req;
|
||||
const fields = body.fields;
|
||||
|
||||
// If the user sends a request when auth is set to false, throw an error
|
||||
if (apiConstants.defaultRoutes.includes(resource) && !config.auth) {
|
||||
return res.status(403).send({
|
||||
message: 'You can not access this endpoint while AUTH is set to false',
|
||||
});
|
||||
}
|
||||
|
||||
// Redirect this request to the registerUser controller => POST /api/tables/_users/rows
|
||||
if (resource === '_users' && method === 'POST') {
|
||||
return registerUser(req, res);
|
||||
}
|
||||
|
||||
// Remove some fields for this request and check the username field => PUT /api/tables/_users/rows
|
||||
if (resource === '_users' && method === 'PUT') {
|
||||
// check if the username is taken
|
||||
if (fields.username) {
|
||||
if (isUsernameTaken(fields.username)) {
|
||||
return res
|
||||
.status(409)
|
||||
.send({ message: 'This username is already taken' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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], [SALT, IS_SUPERUSER, 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') {
|
||||
removeFields(payload.data, [SALT, 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 === 'POST') {
|
||||
if (reservedTableNames.includes(body.name)) {
|
||||
return res.status(409).send({
|
||||
message: `The table name is reserved. Please choose a different name for the table. Table name: ${body.name}.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
processRowRequest,
|
||||
processRowResponse,
|
||||
processTableRequest,
|
||||
};
|
||||
70
src/middlewares/auth.js
Normal file
70
src/middlewares/auth.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const config = require('../config');
|
||||
const { decodeToken, toBoolean } = require('../utils/index');
|
||||
const httpVerbs = require('../constants/httpVerbs');
|
||||
|
||||
const isAuthenticated = async (req, res, next) => {
|
||||
let payload;
|
||||
const { name: tableName } = req.params;
|
||||
const verb = req.method;
|
||||
|
||||
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(403).send({ message: 'Invalid access token' });
|
||||
}
|
||||
|
||||
// if the user is a super_user, allow access on the resource
|
||||
if (toBoolean(payload.isSuperuser)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// if table_name is not passed from the router throw unauthorized error
|
||||
if (!tableName) {
|
||||
return res.status(403).send({ message: 'Not authorized' });
|
||||
}
|
||||
|
||||
// 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: 'Permission not defined for this role' });
|
||||
}
|
||||
|
||||
// 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 = httpVerbs[verb].toLowerCase();
|
||||
|
||||
if (toBoolean(resource[httpMethod])) {
|
||||
hasPermission = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasPermission) {
|
||||
next();
|
||||
} else {
|
||||
return res.status(403).send({ message: 'Not authorized' });
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(401).send({ message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { isAuthenticated };
|
||||
@@ -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;
|
||||
|
||||
|
||||
29
src/routes/auth.js
Normal file
29
src/routes/auth.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const express = require('express');
|
||||
|
||||
const controllers = require('../controllers/auth');
|
||||
const { validator } = require('../middlewares/validation');
|
||||
const schema = require('../schemas/auth');
|
||||
const { isAuthenticated } = 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',
|
||||
validator(schema.changePassword),
|
||||
isAuthenticated,
|
||||
controllers.changePassword,
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -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 { isAuthenticated } = require('../middlewares/auth');
|
||||
const schema = require('../schemas/rows');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/:name/rows',
|
||||
isAuthenticated,
|
||||
validator(schema.listTableRows),
|
||||
controllers.listTableRows
|
||||
processRowRequest,
|
||||
controllers.listTableRows,
|
||||
processRowResponse,
|
||||
);
|
||||
router.post(
|
||||
'/:name/rows',
|
||||
isAuthenticated,
|
||||
validator(schema.insertRowInTable),
|
||||
processRowRequest,
|
||||
controllers.insertRowInTable,
|
||||
broadcast
|
||||
broadcast,
|
||||
);
|
||||
router.get(
|
||||
'/:name/rows/:pks',
|
||||
isAuthenticated,
|
||||
validator(schema.getRowInTableByPK),
|
||||
controllers.getRowInTableByPK
|
||||
controllers.getRowInTableByPK,
|
||||
processRowResponse,
|
||||
);
|
||||
router.put(
|
||||
'/:name/rows/:pks',
|
||||
isAuthenticated,
|
||||
validator(schema.updateRowInTableByPK),
|
||||
processRowRequest,
|
||||
controllers.updateRowInTableByPK,
|
||||
broadcast
|
||||
broadcast,
|
||||
);
|
||||
router.delete(
|
||||
'/:name/rows/:pks',
|
||||
isAuthenticated,
|
||||
validator(schema.deleteRowInTableByPK),
|
||||
controllers.deleteRowInTableByPK,
|
||||
broadcast
|
||||
broadcast,
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -3,16 +3,38 @@ const express = require('express');
|
||||
const controllers = require('../controllers/tables');
|
||||
const { validator } = require('../middlewares/validation');
|
||||
const schema = require('../schemas/tables');
|
||||
const { isAuthenticated } = 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(
|
||||
'/',
|
||||
isAuthenticated,
|
||||
validator(schema.listTables),
|
||||
controllers.listTables,
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
processTableRequest,
|
||||
isAuthenticated,
|
||||
validator(schema.createTable),
|
||||
controllers.createTable,
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:name',
|
||||
isAuthenticated,
|
||||
validator(schema.getTableSchema),
|
||||
controllers.getTableSchema
|
||||
controllers.getTableSchema,
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:name',
|
||||
isAuthenticated,
|
||||
validator(schema.deleteTable),
|
||||
controllers.deleteTable,
|
||||
);
|
||||
router.delete('/:name', validator(schema.deleteTable), controllers.deleteTable);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
37
src/schemas/auth.js
Normal file
37
src/schemas/auth.js
Normal file
@@ -0,0 +1,37 @@
|
||||
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(),
|
||||
});
|
||||
|
||||
const refreshAccessToken = Joi.object({
|
||||
query: Joi.object().required(),
|
||||
params: Joi.object({}).required(),
|
||||
body: Joi.object({}).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(),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
obtainAccessToken,
|
||||
refreshAccessToken,
|
||||
changePassword,
|
||||
};
|
||||
@@ -37,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);
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,22 @@
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Rows"
|
||||
],
|
||||
"tags": ["Rows"],
|
||||
"summary": "Insert Row",
|
||||
"description": "Insert a new row in a table",
|
||||
"parameters": [
|
||||
@@ -303,15 +317,22 @@
|
||||
"schema": {
|
||||
"$ref": "#/definitions/InsertRowErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/tables/{name}/rows/{pks}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Rows"
|
||||
],
|
||||
"tags": ["Rows"],
|
||||
"summary": "Retrieve Row",
|
||||
"description": "Retrieve a row by primary key",
|
||||
"parameters": [
|
||||
@@ -352,21 +373,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 +448,20 @@
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Rows"
|
||||
],
|
||||
"tags": ["Rows"],
|
||||
"summary": "Delete Row",
|
||||
"description": "Delete a row by primary key",
|
||||
"parameters": [
|
||||
@@ -465,11 +494,128 @@
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/token/obtain": {
|
||||
"post": {
|
||||
"tags": ["Auth"],
|
||||
"summary": "Obtain Access Token",
|
||||
"description": "Endpoint to generate access and refresh tokens",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ObtainAccessTokenRequestBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Access token and Refresh token generated",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ObtainAccessTokenSuccessResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid username or password error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/InvalidCredentialErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/token/refresh": {
|
||||
"get": {
|
||||
"tags": ["Auth"],
|
||||
"summary": "Refresh Access Token",
|
||||
"description": "Endpoint to refresh access and refresh tokens",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Access token refreshed",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/RefreshAccessTokenSuccessResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid refresh token error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/InvalidRefreshTokenErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/change-password": {
|
||||
"put": {
|
||||
"tags": ["Auth"],
|
||||
"summary": "Change Password",
|
||||
"description": "Endpoint to change a password",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ChangePasswordRequestBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Weak password error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ChangePasswordSuccessResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Weak password error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/WeakPasswordErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "User not found error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/InvalidPasswordErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
@@ -718,11 +864,7 @@
|
||||
"properties": {
|
||||
"pks": {
|
||||
"type": "array",
|
||||
"example": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"example": [1, 2, 3],
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
@@ -740,11 +882,7 @@
|
||||
"properties": {
|
||||
"pks": {
|
||||
"type": "array",
|
||||
"example": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"example": [1, 2, 3],
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
@@ -753,6 +891,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
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);
|
||||
@@ -11,4 +14,65 @@ const comparePasswords = async (plainPassword, hashedPassword) => {
|
||||
return isMatch;
|
||||
};
|
||||
|
||||
module.exports = { hashPassword, comparePasswords };
|
||||
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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user