Merge pull request #157 from thevahidal/authorization_feature
Add authorization feature + modify unit tests
This commit is contained in:
@@ -9,6 +9,12 @@ 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
|
||||
@@ -16,6 +22,3 @@ START_WITH_STUDIO=false
|
||||
TOKEN_SECRET=ABCD23DCAA
|
||||
ACCESS_TOKEN_EXPIRATION_TIME=10H
|
||||
REFRESH_TOKEN_EXPIRATION_TIME=2D
|
||||
|
||||
INITIAL_USER_USERNAME=superuser
|
||||
INITIAL_USER_PASSWORD=hello@3245CD$
|
||||
|
||||
31
README.md
31
README.md
@@ -35,9 +35,13 @@ Options:
|
||||
-r, --rate-limit-enabled Enable rate limiting [boolean]
|
||||
-c, --cors CORS whitelist origins [string]
|
||||
-a, --auth Enable authentication and authorization [boolean]
|
||||
-ats, --accesstokensecret Access Token Secret [string]
|
||||
|
||||
-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]
|
||||
-rts, --refreshtokensecret Refresh Token Secret [string]
|
||||
-rtet, --refreshtokenexpirationtime Refresh Token Expiration Time [string]
|
||||
-S, --studio Start Soul Studio in parallel
|
||||
--help Show help
|
||||
@@ -58,18 +62,23 @@ To run Soul in auth mode, allowing login and signup features with authorization
|
||||
|
||||
Run the Soul command with the necessary parameters:
|
||||
|
||||
```
|
||||
soul --d foobar.db -a -ts <your_jwt_secret_value> -atet=4H -rtet=3D
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
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 `-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.
|
||||
Teh `-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 `-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
|
||||
|
||||
@@ -129,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.
|
||||
|
||||
12
src/cli.js
12
src/cli.js
@@ -74,6 +74,18 @@ if (process.env.NO_CLI !== 'true') {
|
||||
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',
|
||||
|
||||
@@ -30,6 +30,9 @@ const envVarsSchema = Joi.object()
|
||||
|
||||
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'),
|
||||
@@ -76,6 +79,14 @@ 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);
|
||||
@@ -109,6 +120,11 @@ module.exports = {
|
||||
refreshTokenExpirationTime:
|
||||
argv.refreshtokenexpirationtime || envVars.REFRESH_TOKEN_EXPIRATION_TIME,
|
||||
|
||||
initialUserUsername:
|
||||
argv.initialuserusername || envVars.INITIAL_USER_USERNAME,
|
||||
initialUserPassword:
|
||||
argv.initialuserpassword || envVars.INITIAL_USER_PASSWORD,
|
||||
|
||||
rateLimit: {
|
||||
enabled: argv['rate-limit-enabled'] || envVars.RATE_LIMIT_ENABLED,
|
||||
windowMs: envVars.RATE_LIMIT_WINDOW,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
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: {
|
||||
|
||||
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 dbConstants = require('./tables');
|
||||
const apiConstants = require('./api');
|
||||
const constantRoles = require('./roles');
|
||||
|
||||
module.exports = { dbTables, apiConstants, constantRoles };
|
||||
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,4 +1,6 @@
|
||||
const { tableService, rowService } = require('../services');
|
||||
const { constantRoles, apiConstants, dbConstants } = require('../constants');
|
||||
const schema = require('../db/schema');
|
||||
const config = require('../config');
|
||||
const {
|
||||
hashPassword,
|
||||
@@ -6,40 +8,53 @@ const {
|
||||
comparePasswords,
|
||||
generateToken,
|
||||
decodeToken,
|
||||
toBoolean,
|
||||
} = require('../utils');
|
||||
|
||||
const { dbTables, constantRoles, apiConstants } = require('../constants');
|
||||
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;
|
||||
@@ -49,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',
|
||||
@@ -77,7 +92,7 @@ const createDefaultTables = async () => {
|
||||
|
||||
// store the permissions in the db
|
||||
rowService.bulkWrite({
|
||||
tableName: '_roles_permissions',
|
||||
tableName: ROLE_PERMISSIONS_TABLE,
|
||||
fields: permissions,
|
||||
});
|
||||
}
|
||||
@@ -91,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],
|
||||
});
|
||||
@@ -131,7 +146,7 @@ const updateSuperuser = async (fields) => {
|
||||
|
||||
// update the user
|
||||
rowService.update({
|
||||
tableName: '_users',
|
||||
tableName: USER_TABLE,
|
||||
lookupField: `id`,
|
||||
fieldsString,
|
||||
pks: `${id}`,
|
||||
@@ -147,18 +162,48 @@ const updateSuperuser = async (fields) => {
|
||||
};
|
||||
|
||||
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: '_users',
|
||||
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
|
||||
@@ -170,6 +215,15 @@ const registerUser = async (req, res) => {
|
||||
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
|
||||
@@ -177,7 +231,7 @@ const registerUser = async (req, res) => {
|
||||
|
||||
// create the user
|
||||
const newUser = rowService.save({
|
||||
tableName: '_users',
|
||||
tableName: USER_TABLE,
|
||||
fields: {
|
||||
username,
|
||||
salt,
|
||||
@@ -188,7 +242,7 @@ const registerUser = async (req, res) => {
|
||||
|
||||
// find the default role from the DB
|
||||
let defaultRole = rowService.get({
|
||||
tableName: '_roles',
|
||||
tableName: ROLE_TABLE,
|
||||
whereString: 'WHERE name=?',
|
||||
whereStringValues: [constantRoles.DEFAULT_ROLE],
|
||||
});
|
||||
@@ -197,15 +251,32 @@ const registerUser = async (req, res) => {
|
||||
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',
|
||||
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 });
|
||||
@@ -213,13 +284,26 @@ const registerUser = async (req, res) => {
|
||||
};
|
||||
|
||||
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
|
||||
let users = rowService.get({
|
||||
tableName: '_users',
|
||||
const users = rowService.get({
|
||||
tableName: USER_TABLE,
|
||||
whereString: 'WHERE username=?',
|
||||
whereStringValues: [username],
|
||||
});
|
||||
@@ -229,28 +313,53 @@ const obtainAccessToken = async (req, res) => {
|
||||
}
|
||||
|
||||
// check if the password is valid
|
||||
let user = users[0];
|
||||
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'
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// get the user roles from the DB
|
||||
const userRoles = rowService.get({
|
||||
tableName: '_users_roles',
|
||||
whereString: 'WHERE user_id=?',
|
||||
whereStringValues: [user.id],
|
||||
});
|
||||
let userRoles, permissions, roleIds;
|
||||
|
||||
if (userRoles < 0) {
|
||||
return res.status(404).send({ message: 'Default role not found' });
|
||||
// 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,
|
||||
roleId: userRoles,
|
||||
isSuperuser: user.is_superuser,
|
||||
roleIds,
|
||||
permissions,
|
||||
};
|
||||
|
||||
// generate an access token
|
||||
@@ -273,6 +382,15 @@ const obtainAccessToken = async (req, res) => {
|
||||
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({
|
||||
@@ -283,6 +401,12 @@ const obtainAccessToken = async (req, res) => {
|
||||
};
|
||||
|
||||
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(
|
||||
@@ -292,7 +416,7 @@ const refreshAccessToken = async (req, res) => {
|
||||
|
||||
// find the user
|
||||
const users = rowService.get({
|
||||
tableName: '_users',
|
||||
tableName: USER_TABLE,
|
||||
whereString: 'WHERE id=?',
|
||||
whereStringValues: [payload.userId],
|
||||
});
|
||||
@@ -301,25 +425,56 @@ const refreshAccessToken = async (req, res) => {
|
||||
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];
|
||||
|
||||
const newPaylod = {
|
||||
username: payload.username,
|
||||
userId: payload.userId,
|
||||
roleId: payload.roleId,
|
||||
// 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', ...newPaylod },
|
||||
{ subject: 'accessToken', ...newPayload },
|
||||
config.tokenSecret,
|
||||
config.accessTokenExpirationTime,
|
||||
);
|
||||
|
||||
// generate a refresh token
|
||||
const refreshToken = await generateToken(
|
||||
{ subject: 'refreshToken', ...newPaylod },
|
||||
{ subject: 'refreshToken', ...newPayload },
|
||||
config.tokenSecret,
|
||||
config.refreshTokenExpirationTime,
|
||||
);
|
||||
@@ -329,20 +484,52 @@ const refreshAccessToken = async (req, res) => {
|
||||
res.cookie('accessToken', accessToken, cookieOptions);
|
||||
res.cookie('refreshToken', refreshToken, cookieOptions);
|
||||
|
||||
res.status(201).send({ message: 'Success', data: { userId: user.id } });
|
||||
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(401).send({ message: 'Invalid refresh token' });
|
||||
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: '_users',
|
||||
tableName: USER_TABLE,
|
||||
whereString: 'WHERE id=?',
|
||||
whereStringValues: [userInfo.userId],
|
||||
});
|
||||
@@ -361,6 +548,14 @@ const changePassword = async (req, res) => {
|
||||
|
||||
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
|
||||
@@ -372,6 +567,15 @@ const changePassword = async (req, res) => {
|
||||
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
|
||||
@@ -382,21 +586,119 @@ const changePassword = async (req, res) => {
|
||||
|
||||
// update the user
|
||||
rowService.update({
|
||||
tableName: '_users',
|
||||
tableName: USER_TABLE,
|
||||
lookupField: `id`,
|
||||
fieldsString: `hashed_password = '${hashedPassword}', salt = '${salt}'`,
|
||||
pks: `${user.id}`,
|
||||
});
|
||||
|
||||
res.status(201).send({
|
||||
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,
|
||||
@@ -404,4 +706,6 @@ module.exports = {
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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' },
|
||||
},
|
||||
|
||||
@@ -19,7 +19,11 @@ 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();
|
||||
@@ -75,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.',
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
const config = require('../config');
|
||||
const { registerUser } = require('../controllers/auth');
|
||||
const { apiConstants } = require('../constants/');
|
||||
const { registerUser, isUsernameTaken } = require('../controllers/auth');
|
||||
const { apiConstants, dbConstants } = require('../constants/');
|
||||
const { removeFields } = require('../utils');
|
||||
|
||||
const processRequest = async (req, res, next) => {
|
||||
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 = req.method;
|
||||
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) {
|
||||
@@ -13,15 +18,33 @@ const processRequest = async (req, res, next) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Execute user registration function
|
||||
if (resource === '_users' && method === 'POST' && config.auth) {
|
||||
// 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 processResponse = async (req, res, next) => {
|
||||
const processRowResponse = async (req, res, next) => {
|
||||
// Extract payload data
|
||||
const resource = req.params.name;
|
||||
const status = req.response.status;
|
||||
@@ -29,24 +52,30 @@ const processResponse = async (req, res, next) => {
|
||||
|
||||
// Remove some fields from the response
|
||||
if (resource === '_users') {
|
||||
removeFields(payload.data, ['salt', 'hashed_password']);
|
||||
removeFields(payload.data, [SALT, HASHED_PASSWORD]);
|
||||
}
|
||||
|
||||
res.status(status).send(payload);
|
||||
next();
|
||||
};
|
||||
|
||||
const removeFields = async (rows, fields) => {
|
||||
const newPayload = rows.map((row) => {
|
||||
fields.map((field) => {
|
||||
delete row[field];
|
||||
});
|
||||
});
|
||||
const processTableRequest = async (req, res, next) => {
|
||||
const { method, body, baseUrl } = req;
|
||||
|
||||
return newPayload;
|
||||
// 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 = {
|
||||
processRequest,
|
||||
processResponse,
|
||||
processRowRequest,
|
||||
processRowResponse,
|
||||
processTableRequest,
|
||||
};
|
||||
|
||||
@@ -1,18 +1,68 @@
|
||||
const config = require('../config');
|
||||
const { decodeToken } = require('../utils/index');
|
||||
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 {
|
||||
// extract the payload from the token and verify it
|
||||
const payload = await decodeToken(
|
||||
req.cookies.accessToken,
|
||||
config.jwtSecret,
|
||||
);
|
||||
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' });
|
||||
}
|
||||
|
||||
req.user = payload;
|
||||
// if the user is a super_user, allow access on the resource
|
||||
if (toBoolean(payload.isSuperuser)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,39 +3,46 @@ const express = require('express');
|
||||
const controllers = require('../controllers/rows');
|
||||
const { broadcast } = require('../middlewares/broadcast');
|
||||
const { validator } = require('../middlewares/validation');
|
||||
const { processRequest, processResponse } = require('../middlewares/api');
|
||||
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),
|
||||
processRequest,
|
||||
processRowRequest,
|
||||
controllers.listTableRows,
|
||||
processResponse,
|
||||
processRowResponse,
|
||||
);
|
||||
router.post(
|
||||
'/:name/rows',
|
||||
isAuthenticated,
|
||||
validator(schema.insertRowInTable),
|
||||
processRequest,
|
||||
processRowRequest,
|
||||
controllers.insertRowInTable,
|
||||
broadcast,
|
||||
);
|
||||
router.get(
|
||||
'/:name/rows/:pks',
|
||||
isAuthenticated,
|
||||
validator(schema.getRowInTableByPK),
|
||||
controllers.getRowInTableByPK,
|
||||
processResponse,
|
||||
processRowResponse,
|
||||
);
|
||||
router.put(
|
||||
'/:name/rows/:pks',
|
||||
isAuthenticated,
|
||||
validator(schema.updateRowInTableByPK),
|
||||
processRowRequest,
|
||||
controllers.updateRowInTableByPK,
|
||||
broadcast,
|
||||
);
|
||||
router.delete(
|
||||
'/:name/rows/:pks',
|
||||
isAuthenticated,
|
||||
validator(schema.deleteRowInTableByPK),
|
||||
controllers.deleteRowInTableByPK,
|
||||
broadcast,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,7 +20,7 @@ const refreshAccessToken = Joi.object({
|
||||
|
||||
const changePassword = Joi.object({
|
||||
query: Joi.object().required(),
|
||||
params: Joi.object({ userId: Joi.string().required() }).required(),
|
||||
params: Joi.object().required(),
|
||||
|
||||
body: Joi.object({
|
||||
fields: Joi.object({
|
||||
|
||||
@@ -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,6 +19,10 @@
|
||||
{
|
||||
"name": "Rows",
|
||||
"description": "Rows endpoints"
|
||||
},
|
||||
{
|
||||
"name": "Auth",
|
||||
"description": "Auth endpoints"
|
||||
}
|
||||
],
|
||||
"schemes": ["http", "https"],
|
||||
@@ -102,6 +106,12 @@
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -131,6 +141,15 @@
|
||||
"schema": {
|
||||
"$ref": "#/definitions/CreateTableErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +174,12 @@
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -177,6 +202,12 @@
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,8 +273,14 @@
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -281,8 +318,14 @@
|
||||
"$ref": "#/definitions/InsertRowErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,6 +376,12 @@
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found"
|
||||
}
|
||||
@@ -399,6 +448,15 @@
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -436,6 +494,12 @@
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found"
|
||||
}
|
||||
@@ -444,30 +508,34 @@
|
||||
},
|
||||
"/api/auth/token/obtain": {
|
||||
"post": {
|
||||
"description": "",
|
||||
"tags": ["Auth"],
|
||||
"summary": "Obtain Access Token",
|
||||
"description": "Endpoint to generate access and refresh tokens",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fields": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
"$ref": "#/definitions/ObtainAccessTokenRequestBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created"
|
||||
"description": "Access token and Refresh token generated",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ObtainAccessTokenSuccessResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
"description": "Invalid username or password error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/InvalidCredentialErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found"
|
||||
@@ -480,47 +548,68 @@
|
||||
},
|
||||
"/api/auth/token/refresh": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"tags": ["Auth"],
|
||||
"summary": "Refresh Access Token",
|
||||
"description": "Endpoint to refresh access and refresh tokens",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created"
|
||||
"200": {
|
||||
"description": "Access token refreshed",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/RefreshAccessTokenSuccessResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
"description": "Invalid refresh token error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/InvalidRefreshTokenErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/change-password": {
|
||||
"put": {
|
||||
"description": "",
|
||||
"tags": ["Auth"],
|
||||
"summary": "Change Password",
|
||||
"description": "Endpoint to change a password",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fields": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
"$ref": "#/definitions/ChangePasswordRequestBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created"
|
||||
"200": {
|
||||
"description": "Weak password error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ChangePasswordSuccessResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
"description": "Weak password error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/WeakPasswordErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
"description": "User not found error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/InvalidPasswordErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
@@ -802,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 };
|
||||
|
||||
@@ -32,10 +32,47 @@ const decodeToken = async (token, secret) => {
|
||||
}
|
||||
};
|
||||
|
||||
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