Fix login error + Fix merge conflicts

This commit is contained in:
AbegaM
2024-03-04 17:11:21 +03:00
14 changed files with 202 additions and 103 deletions

View File

@@ -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,21 +35,19 @@ Options:
-r, --rate-limit-enabled Enable rate limiting [boolean]
-c, --cors CORS whitelist origins [string]
-a, --auth Enable authentication and authorization [boolean]
-js, --jwtsecret JWT Secret [string]
-jet, --jwtexpirationtime JWT Expiration Time [string]
-suu, --superuserusername Initial user username [string]
-sup, --superuserpassword Initial user password [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]
-rtet, --refreshtokenexpirationtime Refresh Token Expiration Time [string]
-S, --studio Start Soul Studio in parallel
--help Show help
```
NOTE: When specifying the JWT expiration time in Soul, it must be in a specific format. Here are some examples:
60M: Represents a duration of 60 minutes.
5H: Represents a duration of 5 hours.
1D: Represents a duration of 1 day.
Then to test Soul is working run the following command
```bash
@@ -56,36 +56,46 @@ curl http://localhost:8000/api/tables
It should return a list of the tables inside `sqlite.db` database.
**Running Soul in Auth mode**
### 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 -js=<your_jwt_secret_value> -jet=3D -iuu=john -iup=<your_password>
soul --d foobar.db -a -ats <your_jwt_access_token_secret_value> -atet=4H -rts <your_jwt_refresh_token_secret_value> -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 enables Soul to run in auth mode.
The `-js` flag allows you to pass a JWT secret value for token generation and verification. Replace <your_jwt_secret_value> with your desired secret value.
The `-jet` flag sets the JWT expiration time. In this case, it is set to one day (3D), meaning the tokens will expire after 72 hours. (`jet` is used for the JWT Refresh Token)
The `-a` flag instructs Soul to run in auth mode.
The `-ats` flag allows you to pass a JWT secret value for the `access token` generation and verification. Replace <your_jwt_access_token_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 `-rts` flag allows you to pass a JWT secret value for the `refresh token` generation and verification. Replace <your_jwt_refresh_token_secret_value> with your desired secret value.
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
The `-iup` flag is used to pass a password for the initial user
**NOTE: It is crucial to securely store a copy of the JWT secret value used in Soul. Once you pass this value, make sure to keep a backup because you will need it every time you restart Soul. Losing this secret value can result in a situation where all of your users are blocked from accessing Soul.**
Here are some example values for the `-atet` and `rtet` flags
**Updating Super Users**
- 60M: Represents a duration of 60 minutes.
- 5H: Represents a duration of 5 hours.
- 1D: Represents a duration of 1 day.
To modify user information in a database, you can utilize the `updateuser` command. This command allows you to change a user's `password` and upgrade a normal user to a `superuser`. Below is an example of how to use it:
NOTE: It is crucial to securely store a copy of the `Access token secret` and `Refresh token secret` values used in Soul. Once you pass this values, make sure to keep a backup because you will need it every time you restart Soul. Losing this secret values can result in a situation where all of your users are blocked from accessing Soul.
### 3. Updating Super Users
To modify a superuser information in a database, you can utilize the `updatesuperuser` command. This command allows you to change a superuser's `password` or upgrade/downgrade a normal user to a `superuser`. Below is an example of how to use it:
```
soul --d foobar.db updateuser --id=1 password=<new_password_for_the_user> // Update the password for the user with ID 1
soul --d foobar.db updatesuperuser --id=1 password=<new_password_for_the_user> // Update the password for the superuser with ID 1
soul --d foobar.db updateuser --id=1 --is_superuser=true // Upgrade the user with ID 1 to a superuser
soul --d foobar.db updatesuperuser --id=1 --is_superuser=true // Upgrade the user with ID 1 to a superuser
soul --d foobar.db updateuser --id=1 --is_superuser=false // Revoke the superuser role from the user with ID 1
soul --d foobar.db updatesuperuser --id=1 --is_superuser=false // Revoke the superuser role from the superuser with ID 1
```
## Documentation

View File

@@ -53,16 +53,30 @@ if (process.env.NO_CLI !== 'true') {
default: false,
demandOption: false,
})
.options('js', {
alias: 'jwtsecret',
describe: 'JWT secret',
.options('ats', {
alias: 'accesstokensecret',
describe: 'JWT secret for access token',
type: 'string',
default: null,
demandOption: false,
})
.options('jet', {
alias: 'jwtexpirationtime',
describe: 'JWT expiration time',
.options('atet', {
alias: 'accesstokenexpirationtime',
describe: 'JWT expiration time for access token',
type: 'string',
default: '5H',
demandOption: false,
})
.options('rts', {
alias: 'refreshtokensecret',
describe: 'JWT secret for refresh token',
type: 'string',
default: null,
demandOption: false,
})
.options('rtet', {
alias: 'refreshtokenexpirationtime',
describe: 'JWT expiration time for refresh token',
type: 'string',
default: '3D',
demandOption: false,
@@ -85,20 +99,20 @@ if (process.env.NO_CLI !== 'true') {
type: 'boolean',
demandOption: false,
})
.command('updateuser', 'Update a user', (yargs) => {
.command('updatesuperuser', 'Update a superuser', (yargs) => {
return yargs
.option('id', {
describe: 'The ID of the user you want to update',
describe: 'The ID of the superuser you want to update',
type: 'number',
demandOption: true,
})
.option('password', {
describe: 'The new password for the user you want to update',
describe: 'The new password for the superuser you want to update',
type: 'string',
demandOption: false,
})
.option('is_superuser', {
describe: 'The role of the user you want to update',
describe: 'The role of the superuser you want to update',
type: 'boolean',
demandOption: false,
});

22
src/commands.js Normal file
View File

@@ -0,0 +1,22 @@
const { yargs } = require('./cli');
const { updateSuperuser } = require('./controllers/auth');
const { argv } = yargs;
const runCLICommands = () => {
//If the updatesuperuser command is passed from the CLI execute the updatesuperuser function
if (argv._.includes('updatesuperuser')) {
const { id, password, is_superuser } = argv;
if (!password && !is_superuser) {
console.log(
'Please provide either the --password or --is_superuser flag when using the updateuser command.',
);
process.exit(1);
} else {
updateSuperuser({ id, password, is_superuser });
}
}
};
module.exports = { runCLICommands };

View File

@@ -30,11 +30,13 @@ const envVarsSchema = Joi.object()
START_WITH_STUDIO: Joi.boolean().default(false),
JWT_SECRET: Joi.string().default(null),
JWT_EXPIRATION_TIME: Joi.string().default('1D'),
INITIAL_USER_USERNAME: Joi.string(),
INITIAL_USER_PASSWORD: Joi.string(),
ACCESS_TOKEN_SECRET: Joi.string().default(null),
ACCESS_TOKEN_EXPIRATION_TIME: Joi.string().default('5H'),
REFRESH_TOKEN_SECRET: Joi.string().default(null),
REFRESH_TOKEN_EXPIRATION_TIME: Joi.string().default('3D'),
})
.unknown();
@@ -66,12 +68,20 @@ if (argv['rate-limit-enabled']) {
env.RATE_LIMIT_ENABLED = argv['rate-limit-enabled'];
}
if (argv.jwtsecret) {
env.JWT_SECRET = argv.jwtsecret;
if (argv.accesstokensecret) {
env.ACCESS_TOKEN_SECRET = argv.accesstokensecret;
}
if (argv.jwtexpirationtime) {
env.JWT_EXPIRATION_TIME = argv.jwtexpirationtime;
if (argv.accesstokenexpirationtime) {
env.ACCESS_TOKEN_EXPIRATION_TIME = argv.accesstokenexpirationtime;
}
if (argv.refreshtokensecret) {
env.REFRESH_TOKEN_SECRET = argv.refreshtokensecret;
}
if (argv.refreshtokenexpirationtime) {
env.REFRESH_TOKEN_EXPIRATION_TIME = argv.refreshtokenexpirationtime;
}
if (argv.initialuserusername) {
@@ -109,8 +119,12 @@ module.exports = {
},
auth: argv.auth || envVars.AUTH,
jwtSecret: argv.jwtsecret || envVars.JWT_SECRET,
jwtExpirationTime: argv.jwtexpirationtime || envVars.JWT_EXPIRATION_TIME,
accessTokenSecret: argv.accesstokensecret || envVars.ACCESS_TOKEN_SECRET,
accessTokenExpirationTime:
argv.accesstokenexpirationtime || envVars.ACCESS_TOKEN_EXPIRATION_TIME,
refreshTokenSecret: argv.refreshtokensecret || envVars.REFRESH_TOKEN_SECRET,
refreshTokenExpirationTime:
argv.refreshtokenexpirationtime || envVars.REFRESH_TOKEN_EXPIRATION_TIME,
initialUserUsername:
argv.initialuserusername || envVars.INITIAL_USER_USERNAME,

3
src/constants/api.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
defaultRoutes: ['_users', '_roles', '_roles_permissions', '_users_roles'],
};

View File

@@ -1,3 +1,5 @@
const dbTables = require('./dbTables');
const apiConstants = require('./api');
const constantRoles = require('./roles');
module.exports = { dbTables };
module.exports = { dbTables, apiConstants, constantRoles };

3
src/constants/roles.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
DEFAULT_ROLE: 'default',
};

View File

@@ -1,6 +1,6 @@
const { tableService } = require('../services');
const { rowService } = require('../services');
const { dbTables } = require('../constants');
const { dbTables, constantRoles } = require('../constants');
const config = require('../config');
const {
hashPassword,
@@ -12,6 +12,8 @@ const {
} = require('../utils');
const createDefaultTables = async () => {
let roleId;
// check if the default tables are already created
const roleTable = tableService.checkTableExists('_roles');
const usersTable = tableService.checkTableExists('_users');
@@ -19,11 +21,13 @@ const createDefaultTables = async () => {
tableService.checkTableExists('_roles_permissions');
const usersRolesTable = tableService.checkTableExists('_users_roles');
// create _users table
if (!usersTable) {
// create the _users table
tableService.createTable('_users', dbTables.userSchema);
}
// create _users_roles table
if (!usersRolesTable) {
// create the _users_roles table
tableService.createTable(
@@ -39,17 +43,21 @@ const createDefaultTables = async () => {
);
}
if (!roleTable && !rolesPermissionTable) {
// create _roles table
if (!roleTable) {
// create the _role table
tableService.createTable('_roles', dbTables.roleSchema);
// create a default role in the _roles table
const role = rowService.save({
tableName: '_roles',
fields: { name: 'default' },
fields: { name: constantRoles.DEFAULT_ROLE },
});
const roleId = role.lastInsertRowid;
roleId = role.lastInsertRowid;
}
// create _roles_permissions table
if (!rolesPermissionTable && roleId) {
// create the _roles_permissions table
tableService.createTable(
'_roles_permissions',
@@ -86,7 +94,7 @@ const createDefaultTables = async () => {
}
};
const updateUser = async (fields) => {
const updateSuperuser = async (fields) => {
const { id, password, is_superuser } = fields;
let newHashedPassword, newSalt;
let fieldsString = '';
@@ -268,15 +276,15 @@ const obtainAccessToken = async (req, res) => {
// generate an access token
const accessToken = await generateToken(
{ subject: 'accessToken', ...payload },
config.jwtSecret,
'1H',
config.accessTokenSecret,
config.accessTokenExpirationTime,
);
// generate a refresh token
const refreshToken = await generateToken(
{ subject: 'refreshToken', ...payload },
config.jwtSecret,
config.jwtExpirationTime,
config.refreshTokenSecret,
config.refreshTokenExpirationTime,
);
// set the token in the cookie
@@ -299,7 +307,7 @@ const refreshAccessToken = async (req, res) => {
// extract the payload from the token and verify it
const payload = await decodeToken(
req.cookies.refreshToken,
config.jwtSecret,
config.refreshTokenSecret,
);
// find the user
@@ -310,28 +318,52 @@ const refreshAccessToken = async (req, res) => {
});
if (users.length <= 0) {
return res.status(401).send({ message: 'User not found' });
return res
.status(401)
.send({ message: `User with userId = ${payload.userId} not found` });
}
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',
whereString: 'WHERE user_id=?',
whereStringValues: [user.id],
});
roleIds = userRoles.map((role) => role.role_id);
// get the permission of the role
permissions = rowService.get({
tableName: '_roles_permissions',
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 },
config.jwtSecret,
'1H',
{ subject: 'accessToken', ...newPayload },
config.accessTokenSecret,
config.accessTokenExpirationTime,
);
// generate a refresh token
const refreshToken = await generateToken(
{ subject: 'refreshToken', ...newPaylod },
config.jwtSecret,
config.jwtExpirationTime,
{ subject: 'refreshToken', ...newPayload },
config.refreshTokenSecret,
config.refreshTokenExpirationTime,
);
// set the token in the cookie
@@ -475,13 +507,12 @@ const createInitialUser = async () => {
}
} catch (error) {
console.log(error);
process.exit(1);
}
};
module.exports = {
createDefaultTables,
updateUser,
updateSuperuser,
registerUser,
obtainAccessToken,
refreshAccessToken,

View File

@@ -19,7 +19,7 @@ describe('Rows Endpoints', () => {
it('GET /tables/:name/rows should return a list of all rows', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -39,7 +39,7 @@ 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.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -80,7 +80,7 @@ describe('Rows Endpoints', () => {
it('GET /tables/:name/rows: should return a null field', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -96,7 +96,7 @@ describe('Rows Endpoints', () => {
it('GET /tables/:name/rows: should successfully retrieve users created after 2010-01-01 00:00:00.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -121,7 +121,7 @@ describe('Rows Endpoints', () => {
it('GET /tables/:name/rows: should successfully retrieve users created before 2008-01-20 00:00:00.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -146,7 +146,7 @@ describe('Rows Endpoints', () => {
it('GET /tables/:name/rows: should successfully retrieve users created at 2013-01-08 00:00:00', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -171,7 +171,7 @@ describe('Rows Endpoints', () => {
it('GET /tables/:name/rows: should successfully retrieve users created at 2007-01-08 00:00:00', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -188,7 +188,7 @@ describe('Rows Endpoints', () => {
it('GET /tables/:name/rows: should successfully retrieve users that are not created at 2021-01-08 00:00:00', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -213,7 +213,7 @@ 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.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -230,7 +230,7 @@ describe('Rows Endpoints', () => {
it('GET /tables/:name/rows/:pks should return a row by its primary key', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -249,7 +249,7 @@ 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.jwtSecret,
config.accessTokenSecret,
'1H',
);
const res = await requestWithSupertest
@@ -263,7 +263,7 @@ describe('Rows Endpoints', () => {
it('DELETE /tables/:name/rows/:pks should delete a row by its primary key and return the number of changes', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -277,7 +277,7 @@ describe('Rows Endpoints', () => {
it('POST /tables/:name/rows should insert a new row if any of the value of the object being inserted is null', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
const res = await requestWithSupertest
@@ -299,7 +299,7 @@ describe('Rows Endpoints', () => {
it('GET /tables/:name/rows should return values if any of the IDs from the array match the user ID.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -315,7 +315,7 @@ describe('Rows Endpoints', () => {
it('GET /tables/:name/rows should return values if the provided ID matches the user ID.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);

View File

@@ -10,7 +10,7 @@ describe('Tables Endpoints', () => {
it('GET /tables should return a list of all tables', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -28,7 +28,7 @@ describe('Tables Endpoints', () => {
it('POST /tables should create a new table and return generated schema', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);
@@ -76,7 +76,7 @@ describe('Tables Endpoints', () => {
it('GET /tables/:name should return schema of the table', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.jwtSecret,
config.accessTokenSecret,
'1H',
);

View File

@@ -21,14 +21,12 @@ const swaggerFile = require('./swagger/swagger.json');
const { setupExtensions } = require('./extensions');
const {
createDefaultTables,
updateUser,
createInitialUser,
} = require('./controllers/auth');
const { yargs } = require('./cli');
const { runCLICommands } = require('./commands');
const app = express();
const { argv } = yargs;
app.get('/health', (req, res) => {
res.send('OK');
});
@@ -91,19 +89,8 @@ if (config.auth) {
);
}
//If the updateuser command is passed from the CLI execute the updateuser function
if (argv._.includes('updateuser')) {
const { id, password, is_superuser } = argv;
if (!password && !is_superuser) {
console.log(
'Please provide either the --password or --is_superuser flag when using the updateuser command.',
);
process.exit(1);
} else {
updateUser({ id, password, is_superuser });
}
}
// If the user has passed custom CLI commands run the command and exit to avoid running the server
runCLICommands();
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerFile));
app.use('/api', rootRoutes);

View File

@@ -1,11 +1,20 @@
const config = require('../config');
const { registerUser } = require('../controllers/auth');
const { apiConstants } = require('../constants/');
const processRequest = async (req, res, next) => {
const resource = req.params.name;
const method = req.method;
// 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(401).send({
message: 'You can not access this endpoint while AUTH is set to false',
});
}
// Execute user registration function
if (resource === '_users' && method === 'POST') {
if (resource === '_users' && method === 'POST' && config.auth) {
return registerUser(req, res);
}

View File

@@ -11,7 +11,10 @@ const isAuthorized = async (req, res, next) => {
if (config.auth) {
// extract the payload from the token and verify it
try {
payload = await decodeToken(req.cookies.accessToken, config.jwtSecret);
payload = await decodeToken(
req.cookies.accessToken,
config.accessTokenSecret,
);
req.user = payload;
} catch (error) {
return res.status(403).send({ message: 'Invalid access token' });
@@ -59,6 +62,7 @@ const isAuthorized = async (req, res, next) => {
next();
}
} catch (error) {
console.log(error);
res.status(401).send({ message: error.message });
}
};

View File

@@ -17,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);
}