Add auth service + move string values to constant folders + Add error messages to constants + setup a folder for auth controller + modify schema + fix comments + modify validator

This commit is contained in:
AbegaM
2024-03-18 17:54:15 +03:00
parent 31e88de507
commit 2e69bfb304
28 changed files with 1099 additions and 928 deletions

View File

View File

@@ -1,14 +1,8 @@
module.exports = {
defaultRoutes: ['_users', '_roles', '_roles_permissions', '_users_roles'],
authEndpoints: ['_users', '_roles', '_roles_permissions', '_users_roles'],
baseTableUrl: '/api/tables',
universalAccessEndpoints: ['/api/auth/change-password'],
fields: {
_users: {
SALT: 'salt',
IS_SUPERUSER: 'is_superuser',
HASHED_PASSWORD: 'hashed_password',
},
},
DEFAULT_PAGE_LIMIT: 10,
DEFAULT_PAGE_INDEX: 0,
PASSWORD: {
@@ -17,6 +11,13 @@ module.exports = {
},
httpVerbs: {
POST: 'POST',
GET: 'GET',
PUT: 'PUT',
DELETE: 'DELETE',
},
httpMethodDefinitions: {
POST: 'CREATE',
GET: 'READ',
PUT: 'UPDATE',

5
src/constants/auth.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
SALT_ROUNDS: 10,
ACCESS_TOKEN_SUBJECT: 'accessToken',
REFRESH_TOKEN_SUBJECT: 'refreshToken',
};

View File

@@ -1,5 +1,13 @@
const dbConstants = require('./tables');
const apiConstants = require('./api');
const constantRoles = require('./roles');
const responseMessages = require('./messages');
const authConstants = require('./auth');
module.exports = { dbConstants, apiConstants, constantRoles };
module.exports = {
dbConstants,
apiConstants,
constantRoles,
responseMessages,
authConstants,
};

41
src/constants/messages.js Normal file
View File

@@ -0,0 +1,41 @@
module.exports = {
successMessage: {
SUCCESS: 'Success',
ROW_INSERTED: 'Row Inserted',
PASSWORD_UPDATE_SUCCESS: 'Password updated successfully',
USER_UPDATE_SUCCESS: 'User updated successfully',
INITIAL_USER_CREATED_SUCCESS: 'Initial user created successfully',
},
errorMessage: {
USERNAME_TAKEN_ERROR: 'This username is taken',
WEAK_PASSWORD_ERROR: 'This password is weak, please use another password',
DEFAULT_ROLE_NOT_CREATED_ERROR:
'Please restart soul so a default role can be created',
INVALID_USERNAME_PASSWORD_ERROR: 'Invalid username or password',
INVALID_REFRESH_TOKEN_ERROR: 'Invalid refresh token',
INVALID_ACCESS_TOKEN_ERROR: 'Invalid access token',
USER_NOT_FOUND_ERROR: 'User not found',
INVALID_CURRENT_PASSWORD_ERROR: 'Invalid current password',
NOT_AUTHORIZED_ERROR: 'Not authorized',
PERMISSION_NOT_DEFINED_ERROR: 'Permission not defined for this role',
ROLE_NOT_FOUND_ERROR: 'Role not found for this user',
AUTH_SET_TO_FALSE_ERROR:
'You can not access this endpoint while AUTH is set to false',
RESERVED_TABLE_NAME_ERROR:
'The table name is reserved. Please choose a different name for the table.',
SERVER_ERROR: 'Server error',
INITIAL_USER_USERNAME_NOT_PASSED_ERROR:
'Error: You should pass the initial users username either from the CLI with the --iuu or from the environment variable using the INITIAL_USER_USERNAME flag',
INITIAL_USER_PASSWORD_NOT_PASSED_ERROR:
'Error: You should pass the initial users password either from the CLI with the --iup or from the environment variable using the INITIAL_USER_PASSWORD flag',
USERNAME_REQUIRED_ERROR: 'username is required',
PASSWORD_REQUIRED_ERROR: 'password is required',
},
infoMessage: {
INITIAL_USER_ALREADY_CREATED: 'Initial user is already created',
},
};

View File

@@ -1,13 +1,47 @@
const USERS_TABLE = '_users';
const ROLES_TABLE = '_roles';
const USERS_ROLES_TABLE = '_users_roles';
const ROLES_PERMISSIONS_TABLE = '_roles_permissions';
module.exports = {
USERS_TABLE,
ROLES_TABLE,
USERS_ROLES_TABLE,
ROLES_PERMISSIONS_TABLE,
reservedTableNames: [
'_users',
'_roles',
'_roles_permissions',
'_users_roles',
USERS_TABLE,
ROLES_TABLE,
USERS_ROLES_TABLE,
ROLES_PERMISSIONS_TABLE,
],
USER_TABLE: '_users',
ROLE_TABLE: '_roles',
USERS_ROLES_TABLE: '_users_roles',
ROLE_PERMISSIONS_TABLE: '_roles_permissions',
constraints: {
UNIQUE_USERS_ROLE: 'unique_users_role',
UNIQUE_ROLES_TABLE: 'unique_ROLES_TABLE',
},
tableFields: {
ID: 'id',
// _role fields
ROLE_NAME: 'name',
// _user fields
USERNAME: 'username',
HASHED_PASSWORD: 'hashed_password',
SALT: 'salt',
IS_SUPERUSER: 'is_superuser',
// _roles_permissions fields
ROLE_ID: 'role_id',
TABLE_NAME: 'table_name',
CREATE: 'create',
READ: 'read',
UPDATE: 'update',
DELETE: 'delete',
// _users_roles fields
USER_ID: 'user_id',
},
};

View File

@@ -1,729 +0,0 @@
const { tableService, rowService } = require('../services');
const { constantRoles, apiConstants, dbConstants } = require('../constants');
const schema = require('../db/schema');
const config = require('../config');
const {
hashPassword,
checkPasswordStrength,
comparePasswords,
generateToken,
decodeToken,
toBoolean,
} = require('../utils');
const { USER_TABLE, ROLE_TABLE, USERS_ROLES_TABLE, ROLE_PERMISSIONS_TABLE } =
dbConstants;
const createDefaultTables = async () => {
let roleId;
// check if the default tables are already created
const roleTable = tableService.checkTableExists(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) {
tableService.createTable(USER_TABLE, schema.userSchema);
}
// create _users_roles table
if (!usersRolesTable) {
tableService.createTable(
USERS_ROLES_TABLE,
schema.usersRoleSchema,
{
multipleUniqueConstraints: {
name: 'unique_users_role',
fields: ['user_id', 'role_id'],
},
},
);
}
// create _roles table
if (!roleTable) {
tableService.createTable(ROLE_TABLE, schema.roleSchema);
// create a default role in the _roles table
const role = rowService.save({
tableName: ROLE_TABLE,
fields: { name: constantRoles.DEFAULT_ROLE },
});
roleId = role.lastInsertRowid;
}
// create _roles_permissions table
if (!rolesPermissionTable && roleId) {
tableService.createTable(
ROLE_PERMISSIONS_TABLE,
schema.rolePermissionSchema,
{
multipleUniqueConstraints: {
name: 'unique_role_table',
fields: ['role_id', 'table_name'],
},
},
);
// fetch all DB tables
const tables = tableService.listTables();
// add permission for the default role (for each db table)
const permissions = [];
for (const table of tables) {
permissions.push({
role_id: roleId,
table_name: table.name,
create: 'false',
read: 'true',
update: 'false',
delete: 'false',
});
}
// store the permissions in the db
rowService.bulkWrite({
tableName: ROLE_PERMISSIONS_TABLE,
fields: permissions,
});
}
};
const updateSuperuser = async (fields) => {
const { id, password, is_superuser } = fields;
let newHashedPassword, newSalt;
let fieldsString = '';
try {
// find the user by using the id field
const users = rowService.get({
tableName: USER_TABLE,
whereString: 'WHERE id=?',
whereStringValues: [id],
});
// abort if the id is invalid
if (users.length === 0) {
console.log('The user id you passed does not exist in the database');
process.exit(1);
}
// check if the is_superuser field is passed
if (is_superuser !== undefined) {
fieldsString = `is_superuser = '${is_superuser}'`;
}
// if the password is sent from the CLI, update it
if (password) {
// check if the password is weak
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password),
)
) {
console.log('Your password should be at least 8 charachters long');
process.exit(1);
}
//hash the password
const { hashedPassword, salt } = await hashPassword(password, 10);
newHashedPassword = hashedPassword;
newSalt = salt;
fieldsString = `${
fieldsString ? fieldsString + ', ' : ''
}hashed_password = '${newHashedPassword}', salt = '${newSalt}'`;
}
// update the user
rowService.update({
tableName: USER_TABLE,
lookupField: `id`,
fieldsString,
pks: `${id}`,
});
console.log(
'User updated successfully, you can now restart soul without the updateuser command',
);
process.exit(1);
} catch (error) {
console.log(error);
}
};
const registerUser = async (req, res) => {
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Register User'
#swagger.description = 'Endpoint to signup'
#swagger.parameters['username'] = {
in: 'body',
required: true,
type: 'object',
schema: { $ref: '#/definitions/UserRegistrationRequestBody' }
}
*/
const { username, password } = req.body.fields;
try {
if (!username) {
return res.status(400).send({ message: 'username is required' });
}
if (!password) {
return res.status(400).send({ message: 'password is required' });
}
// check if the username is taken
let user = rowService.get({
tableName: USER_TABLE,
whereString: 'WHERE username=?',
whereStringValues: [username],
});
if (user.length > 0) {
return res.status(409).send({ message: 'This username is taken' });
/*
#swagger.responses[409] = {
description: 'Username taken error',
schema: {
$ref: '#/definitions/UsernameTakenErrorResponse'
}
}
*/
}
// check if the password is weak
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password),
)
) {
return res.status(400).send({
message: 'This password is weak, please use another password',
});
/*
#swagger.responses[400] = {
description: 'Weak password error',
schema: {
$ref: '#/definitions/WeakPasswordErrorResponse'
}
}
*/
}
// hash the password
const { salt, hashedPassword } = await hashPassword(password, 10);
// create the user
const newUser = rowService.save({
tableName: USER_TABLE,
fields: {
username,
salt,
hashed_password: hashedPassword,
is_superuser: 'false',
},
});
// find the default role from the DB
let defaultRole = rowService.get({
tableName: ROLE_TABLE,
whereString: 'WHERE name=?',
whereStringValues: [constantRoles.DEFAULT_ROLE],
});
if (defaultRole.length <= 0) {
return res.status(500).send({
message: 'Please restart soul so a default role can be created',
});
/*
#swagger.responses[500] = {
description: 'Server error',
schema: {
$ref: '#/definitions/DefaultRoleNotCreatedErrorResponse'
}
}
*/
}
// create a role for the user
rowService.save({
tableName: USERS_ROLES_TABLE,
fields: { user_id: newUser.lastInsertRowid, role_id: defaultRole[0].id },
});
res.status(201).send({ message: 'Row Inserted' });
/*
#swagger.responses[201] = {
description: 'Row inserted',
schema: {
$ref: '#/definitions/InsertRowSuccessResponse'
}
}
*/
} catch (error) {
console.log(error);
res.status(500).send({ message: error.message });
}
};
const obtainAccessToken = async (req, res) => {
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Obtain Access Token'
#swagger.description = 'Endpoint to generate access and refresh tokens'
#swagger.parameters['body'] = {
in: 'body',
required: true,
type: 'object',
schema: { $ref: '#/definitions/ObtainAccessTokenRequestBody' }
}
*/
// extract payload
const { username, password } = req.body.fields;
try {
// check if the username exists in the Db
const users = rowService.get({
tableName: USER_TABLE,
whereString: 'WHERE username=?',
whereStringValues: [username],
});
if (users.length <= 0) {
return res.status(401).send({ message: 'Invalid username or password' });
}
// check if the password is valid
const user = users[0];
const isMatch = await comparePasswords(password, user.hashed_password);
if (!isMatch) {
return res.status(401).send({ message: 'Invalid username or password' });
/*
#swagger.responses[401] = {
description: 'Invalid username or password error',
schema: {
$ref: '#/definitions/InvalidCredentialErrorResponse'
}
}
*/
}
let userRoles, permissions, roleIds;
// if the user is not a superuser get the role and its permission from the DB
if (!toBoolean(user.is_superuser)) {
userRoles = rowService.get({
tableName: USERS_ROLES_TABLE,
whereString: 'WHERE user_id=?',
whereStringValues: [user.id],
});
if (userRoles <= 0) {
return res
.status(404)
.send({ message: 'Role not found for this user' });
}
roleIds = userRoles.map((role) => role.role_id);
// get the permission of the role
permissions = rowService.get({
tableName: ROLE_PERMISSIONS_TABLE,
whereString: `WHERE role_id IN (${roleIds.map(() => '?')})`,
whereStringValues: [...roleIds],
});
}
const payload = {
username: user.username,
userId: user.id,
isSuperuser: user.is_superuser,
roleIds,
permissions,
};
// generate an access token
const accessToken = await generateToken(
{ subject: 'accessToken', ...payload },
config.tokenSecret,
config.accessTokenExpirationTime,
);
// generate a refresh token
const refreshToken = await generateToken(
{ subject: 'refreshToken', ...payload },
config.tokenSecret,
config.refreshTokenExpirationTime,
);
// set the token in the cookie
let cookieOptions = { httpOnly: true, secure: false, Path: '/' };
res.cookie('accessToken', accessToken, cookieOptions);
res.cookie('refreshToken', refreshToken, cookieOptions);
res.status(201).send({ message: 'Success', data: { userId: user.id } });
/*
#swagger.responses[201] = {
description: 'Access token and Refresh token generated',
schema: {
$ref: '#/definitions/ObtainAccessTokenSuccessResponse'
}
}
*/
} catch (error) {
console.log(error);
return res.status(500).json({
message: error.message,
error: error,
});
}
};
const refreshAccessToken = async (req, res) => {
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Refresh Access Token'
#swagger.description = 'Endpoint to refresh access and refresh tokens'
*/
try {
// extract the payload from the token and verify it
const payload = await decodeToken(
req.cookies.refreshToken,
config.tokenSecret,
);
// find the user
const users = rowService.get({
tableName: USER_TABLE,
whereString: 'WHERE id=?',
whereStringValues: [payload.userId],
});
if (users.length <= 0) {
return res
.status(401)
.send({ message: `User with userId = ${payload.userId} not found` });
/*
#swagger.responses[401] = {
description: 'User not found error',
schema: {
$ref: '#/definitions/UserNotFoundErrorResponse'
}
}
*/
}
let userRoles, permissions, roleIds;
const user = users[0];
// if the user is not a superuser get the role and its permission from the DB
if (!toBoolean(user.is_superuser)) {
userRoles = rowService.get({
tableName: USERS_ROLES_TABLE,
whereString: 'WHERE user_id=?',
whereStringValues: [user.id],
});
roleIds = userRoles.map((role) => role.role_id);
// get the permission of the role
permissions = rowService.get({
tableName: ROLE_PERMISSIONS_TABLE,
whereString: `WHERE role_id IN (${roleIds.map(() => '?')})`,
whereStringValues: [...roleIds],
});
}
const newPayload = {
username: user.username,
userId: user.id,
isSuperuser: user.is_superuser,
roleIds,
permissions,
};
// generate an access token
const accessToken = await generateToken(
{ subject: 'accessToken', ...newPayload },
config.tokenSecret,
config.accessTokenExpirationTime,
);
// generate a refresh token
const refreshToken = await generateToken(
{ subject: 'refreshToken', ...newPayload },
config.tokenSecret,
config.refreshTokenExpirationTime,
);
// set the token in the cookie
let cookieOptions = { httpOnly: true, secure: false, Path: '/' };
res.cookie('accessToken', accessToken, cookieOptions);
res.cookie('refreshToken', refreshToken, cookieOptions);
res.status(200).send({ message: 'Success', data: { userId: user.id } });
/*
#swagger.responses[200] = {
description: 'Access token refreshed',
schema: {
$ref: '#/definitions/RefreshAccessTokenSuccessResponse'
}
}
*/
} catch (error) {
res.status(403).send({ message: 'Invalid refresh token' });
/*
#swagger.responses[401] = {
description: 'Invalid refresh token error',
schema: {
$ref: '#/definitions/InvalidRefreshTokenErrorResponse'
}
}
*/
}
};
const changePassword = async (req, res) => {
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Change Password'
#swagger.description = 'Endpoint to change a password'
#swagger.parameters['body'] = {
in: 'body',
required: true,
type: 'object',
schema: {
$ref: '#/definitions/ChangePasswordRequestBody'
}
}
*/
const userInfo = req.user;
const { currentPassword, newPassword } = req.body.fields;
try {
// get the user from the Db
const users = rowService.get({
tableName: USER_TABLE,
whereString: 'WHERE id=?',
whereStringValues: [userInfo.userId],
});
if (users.length <= 0) {
return res.status(401).send({ message: 'User not found' });
}
const user = users[0];
// check if the users current password is valid
const isMatch = await comparePasswords(
currentPassword,
user.hashed_password,
);
if (!isMatch) {
return res.status(401).send({ message: 'Invalid current password' });
/*
#swagger.responses[401] = {
description: 'User not found error',
schema: {
$ref: '#/definitions/InvalidPasswordErrorResponse'
}
}
*/
}
// check if the new password is strong
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(newPassword),
)
) {
return res.status(400).send({
message: 'This password is weak, please use another password',
});
/*
#swagger.responses[400] = {
description: 'Weak password error',
schema: {
$ref: '#/definitions/WeakPasswordErrorResponse'
}
}
*/
}
// hash the password
const { salt, hashedPassword } = await hashPassword(newPassword, 10);
user.salt = salt;
user.hashed_password = hashedPassword;
// update the user
rowService.update({
tableName: USER_TABLE,
lookupField: `id`,
fieldsString: `hashed_password = '${hashedPassword}', salt = '${salt}'`,
pks: `${user.id}`,
});
res.status(200).send({
message: 'Password updated successfully',
data: { id: user.id, username: user.username },
});
/*
#swagger.responses[200] = {
description: 'Weak password error',
schema: {
$ref: '#/definitions/ChangePasswordSuccessResponse'
}
}
*/
} catch (error) {
res.status(500).send({ message: error.message });
}
};
const createInitialUser = async () => {
// extract some fields from the environment variables or from the CLI
const { initialUserUsername: username, initialUserPassword: password } =
config;
try {
// check if there is are users in the DB
const users = rowService.get({
tableName: USER_TABLE,
whereString: '',
whereStringValues: [],
});
if (users.length <= 0) {
// check if initial users username is passed from the env or CLI
if (!username) {
console.error(
'Error: You should pass the initial users username either from the CLI with the --iuu or from the environment variable using the INITIAL_USER_USERNAME flag',
);
process.exit(1);
}
// check if initial users password is passed from the env or CLI
if (!password) {
console.error(
'Error: You should pass the initial users password either from the CLI with the --iup or from the environment variable using the INITIAL_USER_PASSWORD flag',
);
process.exit(1);
}
// check if the usernmae is taken
const users = rowService.get({
tableName: USER_TABLE,
whereString: 'WHERE username=?',
whereStringValues: [username],
});
if (users.length > 0) {
console.error(
'Error: The username you passed for the initial user is already taken, please use another username',
);
process.exit(1);
}
// check if the password is strong
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password),
)
) {
console.error(
'Error: The password you passed for the initial user is weak, please use another password',
);
process.exit(1);
}
// hash the password
const { hashedPassword, salt } = await hashPassword(password, 10);
// create the initial user
const { lastInsertRowid: userId } = rowService.save({
tableName: USER_TABLE,
fields: {
username,
hashed_password: hashedPassword,
salt,
is_superuser: 'false',
},
});
// get the default role from the DB
const roles = rowService.get({
tableName: ROLE_TABLE,
whereString: 'WHERE name=?',
whereStringValues: [constantRoles.DEFAULT_ROLE],
});
if (roles.length <= 0) {
console.log(
'Default role not found, please restart soul so a default role can be created',
);
process.exit(1);
}
const defaultRoleId = roles[0].id;
// create a _users_role for the initial user
rowService.save({
tableName: USERS_ROLES_TABLE,
fields: { user_id: userId, role_id: defaultRoleId },
});
console.log('Initial user created');
} else {
console.log('Initial user is already created');
}
} catch (error) {
console.log(error);
}
};
const isUsernameTaken = (username) => {
let user = rowService.get({
tableName: USER_TABLE,
whereString: 'WHERE username=?',
whereStringValues: [username],
});
return user.length > 0;
};
module.exports = {
createDefaultTables,
updateSuperuser,
registerUser,
obtainAccessToken,
refreshAccessToken,
changePassword,
createInitialUser,
isUsernameTaken,
};

View File

@@ -182,30 +182,6 @@ describe('Auth Endpoints', () => {
expect(res.body).not.toHaveProperty('salt');
});
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');
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
});
it('DELETE /tables/_users/rows/:id should remove a user', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },

View File

@@ -0,0 +1,16 @@
const { rowService } = require('../../services');
const { dbConstants } = require('../../constants');
const { USERS_TABLE } = dbConstants;
const isUsernameTaken = (username) => {
const users = rowService.get({
tableName: USERS_TABLE,
whereString: 'WHERE username=?',
whereStringValues: [username],
});
return users.length > 0;
};
module.exports = { isUsernameTaken };

View File

@@ -0,0 +1,5 @@
const users = require('./user');
const token = require('./token');
const tables = require('./tables');
module.exports = { ...users, ...token, ...tables };

View File

@@ -0,0 +1,96 @@
const { tableService, rowService } = require('../../services');
const { constantRoles, dbConstants } = require('../../constants');
const schema = require('../../db/schema');
const {
USERS_TABLE,
ROLES_TABLE,
USERS_ROLES_TABLE,
ROLES_PERMISSIONS_TABLE,
constraints,
tableFields,
} = dbConstants;
const createDefaultTables = async () => {
let roleId;
// check if the default tables are already created
const roleTable = tableService.checkTableExists(ROLES_TABLE);
const usersTable = tableService.checkTableExists(USERS_TABLE);
const rolesPermissionTable = tableService.checkTableExists(
ROLES_PERMISSIONS_TABLE,
);
const usersRolesTable = tableService.checkTableExists(USERS_ROLES_TABLE);
// create _users table
if (!usersTable) {
tableService.createTable(USERS_TABLE, schema.userSchema);
}
// create _users_roles table
if (!usersRolesTable) {
tableService.createTable(
USERS_ROLES_TABLE,
schema.usersRoleSchema,
{
multipleUniqueConstraints: {
name: constraints.UNIQUE_USERS_ROLE,
fields: [tableFields.USER_ID, tableFields.USER_ID],
},
},
);
}
// create _roles table
if (!roleTable) {
tableService.createTable(ROLES_TABLE, schema.roleSchema);
// create a default role in the _roles table
const role = rowService.save({
tableName: ROLES_TABLE,
fields: { name: constantRoles.DEFAULT_ROLE },
});
roleId = role.lastInsertRowid;
}
// create _roles_permissions table
if (!rolesPermissionTable && roleId) {
tableService.createTable(
ROLES_PERMISSIONS_TABLE,
schema.rolePermissionSchema,
{
multipleUniqueConstraints: {
name: constraints.UNIQUE_ROLES_TABLE,
fields: [tableFields.ROLE_ID, tableFields.TABLE_NAME],
},
},
);
// fetch all DB tables
const tables = tableService.listTables();
// add permission for the default role (for each db table)
const permissions = [];
for (const table of tables) {
permissions.push({
role_id: roleId,
table_name: table.name,
create: 'false',
read: 'true',
update: 'false',
delete: 'false',
});
}
// store the permissions in the db
rowService.bulkWrite({
tableName: ROLES_PERMISSIONS_TABLE,
fields: permissions,
});
}
};
module.exports = {
createDefaultTables,
};

View File

@@ -0,0 +1,243 @@
const { authService } = require('../../services');
const { responseMessages, authConstants } = require('../../constants');
const config = require('../../config');
const {
comparePasswords,
generateToken,
decodeToken,
toBoolean,
} = require('../../utils');
const { successMessage, errorMessage } = responseMessages;
const obtainAccessToken = async (req, res) => {
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Obtain Access Token'
#swagger.description = 'Endpoint to generate access and refresh tokens'
#swagger.parameters['body'] = {
in: 'body',
required: true,
type: 'object',
schema: { $ref: '#/definitions/ObtainAccessTokenRequestBody' }
}
*/
// extract payload
const { username, password } = req.body.fields;
try {
// check if the username exists in the Db
const users = authService.getUsersByUsername({ username });
if (users.length <= 0) {
return res
.status(401)
.send({ message: errorMessage.INVALID_USERNAME_PASSWORD_ERROR });
}
// check if the password is valid
const user = users[0];
const isMatch = await comparePasswords(password, user.hashed_password);
if (!isMatch) {
return res
.status(401)
.send({ message: errorMessage.INVALID_USERNAME_PASSWORD_ERROR });
/*
#swagger.responses[401] = {
description: 'Invalid username or password error',
schema: {
$ref: '#/definitions/InvalidCredentialErrorResponse'
}
}
*/
}
let permissions, roleIds;
// if the user is not a superuser get the role and its permission from the DB
if (!toBoolean(user.is_superuser)) {
const roleData = getUsersRoleAndPermission({
userId: user.id,
res,
});
permissions = roleData.permissions;
roleIds = roleData.roleIds;
}
const payload = {
username: user.username,
userId: user.id,
isSuperuser: user.is_superuser,
roleIds,
permissions,
};
// generate an access token
const accessToken = await generateToken(
{ subject: authConstants.ACCESS_TOKEN_SUBJECT, ...payload },
config.tokenSecret,
config.accessTokenExpirationTime,
);
// generate a refresh token
const refreshToken = await generateToken(
{ subject: authConstants.REFRESH_TOKEN_SUBJECT, ...payload },
config.tokenSecret,
config.refreshTokenExpirationTime,
);
// set the token in the cookie
let cookieOptions = { httpOnly: true, secure: false, Path: '/' };
res.cookie(authConstants.ACCESS_TOKEN_SUBJECT, accessToken, cookieOptions);
res.cookie(
authConstants.REFRESH_TOKEN_SUBJECT,
refreshToken,
cookieOptions,
);
res
.status(201)
.send({ message: successMessage.SUCCESS, data: { userId: user.id } });
/*
#swagger.responses[201] = {
description: 'Access token and Refresh token generated',
schema: {
$ref: '#/definitions/ObtainAccessTokenSuccessResponse'
}
}
*/
} catch (error) {
console.log(error);
return res.status(500).json({
message: errorMessage.SERVER_ERROR,
});
}
};
const refreshAccessToken = async (req, res) => {
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Refresh Access Token'
#swagger.description = 'Endpoint to refresh access and refresh tokens'
*/
try {
// extract the payload from the token and verify it
const payload = await decodeToken(
req.cookies.refreshToken,
config.tokenSecret,
);
// find the user
const users = authService.getUsersById({ userId: payload.userId });
if (users.length <= 0) {
return res
.status(401)
.send({ message: errorMessage.USER_NOT_FOUND_ERROR });
/*
#swagger.responses[401] = {
description: 'User not found error',
schema: {
$ref: '#/definitions/UserNotFoundErrorResponse'
}
}
*/
}
let permissions, roleIds;
const user = users[0];
// if the user is not a superuser get the role and its permission from the DB
if (!toBoolean(user.is_superuser)) {
const roleData = getUsersRoleAndPermission({
userId: user.id,
res,
});
permissions = roleData.permissions;
roleIds = roleData.roleIds;
}
const newPayload = {
username: user.username,
userId: user.id,
isSuperuser: user.is_superuser,
roleIds,
permissions,
};
// generate an access token
const accessToken = await generateToken(
{ subject: authConstants.ACCESS_TOKEN_SUBJECT, ...newPayload },
config.tokenSecret,
config.accessTokenExpirationTime,
);
// generate a refresh token
const refreshToken = await generateToken(
{ subject: authConstants.REFRESH_TOKEN_SUBJECT, ...newPayload },
config.tokenSecret,
config.refreshTokenExpirationTime,
);
// set the token in the cookie
let cookieOptions = { httpOnly: true, secure: false, Path: '/' };
res.cookie(authConstants.ACCESS_TOKEN_SUBJECT, accessToken, cookieOptions);
res.cookie(
authConstants.REFRESH_TOKEN_SUBJECT,
refreshToken,
cookieOptions,
);
res
.status(200)
.send({ message: successMessage.SUCCESS, data: { userId: user.id } });
/*
#swagger.responses[200] = {
description: 'Access token refreshed',
schema: {
$ref: '#/definitions/RefreshAccessTokenSuccessResponse'
}
}
*/
} catch (error) {
res.status(403).send({ message: errorMessage.INVALID_REFRESH_TOKEN_ERROR });
/*
#swagger.responses[401] = {
description: 'Invalid refresh token error',
schema: {
$ref: '#/definitions/InvalidRefreshTokenErrorResponse'
}
}
*/
}
};
const getUsersRoleAndPermission = ({ userId, res }) => {
const userRoles = authService.getUserRoleByUserId({ userId });
if (userRoles <= 0) {
res.status(401).send({ message: errorMessage.ROLE_NOT_FOUND_ERROR });
throw new Error(errorMessage.ROLE_NOT_FOUND_ERROR);
}
const roleIds = userRoles.map((role) => role.role_id);
// get the permission of the role
const permissions = authService.getPermissionByRoleIds({ roleIds });
return { userRoles, roleIds, permissions };
};
module.exports = {
obtainAccessToken,
refreshAccessToken,
};

View File

@@ -0,0 +1,394 @@
const { rowService, authService } = require('../../services');
const {
apiConstants,
dbConstants,
responseMessages,
authConstants,
} = require('../../constants');
const config = require('../../config');
const {
hashPassword,
checkPasswordStrength,
comparePasswords,
} = require('../../utils');
const { USERS_TABLE, USERS_ROLES_TABLE, tableFields } = dbConstants;
const { SALT_ROUNDS } = authConstants;
const { successMessage, errorMessage, infoMessage } = responseMessages;
const updateSuperuser = async (fields) => {
const { id, password, is_superuser } = fields;
let newHashedPassword, newSalt;
let fieldsString = '';
try {
// find the user by using the id field
const users = authService.getRoleByUserId({ id });
// abort if the id is invalid
if (users.length === 0) {
console.log(errorMessage.USER_NOT_FOUND_ERROR);
process.exit(1);
}
// check if the is_superuser field is passed
if (is_superuser !== undefined) {
fieldsString = `${tableFields.IS_SUPERUSER} = '${is_superuser}'`;
}
// if the password is sent from the CLI, update it
if (password) {
// check if the password is weak
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password),
)
) {
console.log(errorMessage.WEAK_PASSWORD_ERROR);
process.exit(1);
}
//hash the password
const { hashedPassword, salt } = await hashPassword(
password,
SALT_ROUNDS,
);
newHashedPassword = hashedPassword;
newSalt = salt;
fieldsString = `${fieldsString ? fieldsString + ', ' : ''} ${
tableFields.HASHED_PASSWORD
} = '${newHashedPassword}', ${tableFields.SALT} = '${newSalt}'`;
}
// update the user
rowService.update({
tableName: USERS_TABLE,
lookupField: tableFields.ID,
fieldsString,
pks: `${id}`,
});
console.log(successMessage.USER_UPDATE_SUCCESS);
process.exit(1);
} catch (error) {
console.log(error);
}
};
const registerUser = async (req, res) => {
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Register User'
#swagger.description = 'Endpoint to signup'
#swagger.parameters['username'] = {
in: 'body',
required: true,
type: 'object',
schema: { $ref: '#/definitions/UserRegistrationRequestBody' }
}
*/
const { username, password } = req.body.fields;
try {
if (!username) {
return res
.status(400)
.send({ message: errorMessage.USERNAME_REQUIRED_ERROR });
}
if (!password) {
return res
.status(400)
.send({ message: errorMessage.PASSWORD_REQUIRED_ERROR });
}
// check if the username is taken
const users = authService.getUsersByUsername({ username });
if (users.length > 0) {
return res
.status(409)
.send({ message: errorMessage.USERNAME_TAKEN_ERROR });
/*
#swagger.responses[409] = {
description: 'Username taken error',
schema: {
$ref: '#/definitions/UsernameTakenErrorResponse'
}
}
*/
}
// check if the password is weak
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password),
)
) {
return res.status(400).send({
message: errorMessage.WEAK_PASSWORD_ERROR,
});
/*
#swagger.responses[400] = {
description: 'Weak password error',
schema: {
$ref: '#/definitions/WeakPasswordErrorResponse'
}
}
*/
}
// hash the password
const { salt, hashedPassword } = await hashPassword(password, SALT_ROUNDS);
// create the user
const newUser = rowService.save({
tableName: USERS_TABLE,
fields: {
username,
salt,
hashed_password: hashedPassword,
is_superuser: 'false',
},
});
// find the default role from the DB
const defaultRole = authService.getDefaultRole();
if (defaultRole.length <= 0) {
return res.status(500).send({
message: errorMessage.DEFAULT_ROLE_NOT_CREATED_ERROR,
});
/*
#swagger.responses[500] = {
description: 'Server error',
schema: {
$ref: '#/definitions/DefaultRoleNotCreatedErrorResponse'
}
}
*/
}
// create a role for the user
rowService.save({
tableName: USERS_ROLES_TABLE,
fields: { user_id: newUser.lastInsertRowid, role_id: defaultRole[0].id },
});
res.status(201).send({ message: successMessage.ROW_INSERTED });
/*
#swagger.responses[201] = {
description: 'Row inserted',
schema: {
$ref: '#/definitions/InsertRowSuccessResponse'
}
}
*/
} catch (error) {
console.log(error);
res.status(500).send({ message: errorMessage.SERVER_ERROR });
}
};
const changePassword = async (req, res) => {
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Change Password'
#swagger.description = 'Endpoint to change a password'
#swagger.parameters['body'] = {
in: 'body',
required: true,
type: 'object',
schema: {
$ref: '#/definitions/ChangePasswordRequestBody'
}
}
*/
const userInfo = req.user;
const { currentPassword, newPassword } = req.body.fields;
try {
// get the user from the Db
const users = authService.getUsersById({ userId: userInfo.userId });
if (users.length <= 0) {
return res
.status(401)
.send({ message: errorMessage.USER_NOT_FOUND_ERROR });
}
const user = users[0];
// check if the users current password is valid
const isMatch = await comparePasswords(
currentPassword,
user.hashed_password,
);
if (!isMatch) {
return res
.status(401)
.send({ message: errorMessage.INVALID_CURRENT_PASSWORD_ERROR });
/*
#swagger.responses[401] = {
description: 'User not found error',
schema: {
$ref: '#/definitions/InvalidPasswordErrorResponse'
}
}
*/
}
// check if the new password is strong
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(newPassword),
)
) {
return res.status(400).send({
message: errorMessage.WEAK_PASSWORD_ERROR,
});
/*
#swagger.responses[400] = {
description: 'Weak password error',
schema: {
$ref: '#/definitions/WeakPasswordErrorResponse'
}
}
*/
}
// hash the password
const { salt, hashedPassword } = await hashPassword(
newPassword,
SALT_ROUNDS,
);
user.salt = salt;
user.hashed_password = hashedPassword;
// update the user
rowService.update({
tableName: USERS_TABLE,
lookupField: tableFields.ID,
fieldsString: `${tableFields.HASHED_PASSWORD} = '${hashedPassword}', ${tableFields.SALT} = '${salt}'`,
pks: `${user.id}`,
});
res.status(200).send({
message: successMessage.PASSWORD_UPDATE_SUCCESS,
data: { id: user.id, username: user.username },
});
/*
#swagger.responses[200] = {
description: 'Weak password error',
schema: {
$ref: '#/definitions/ChangePasswordSuccessResponse'
}
}
*/
} catch (error) {
res.status(500).send({ message: errorMessage.SERVER_ERROR });
}
};
const createInitialUser = async () => {
// extract some fields from the environment variables or from the CLI
const { initialUserUsername: username, initialUserPassword: password } =
config;
try {
// check if there are users in the DB
const users = authService.getAllUsers();
if (users.length <= 0) {
// check if initial users username is passed from the env or CLI
if (!username) {
console.error(errorMessage.INITIAL_USER_USERNAME_NOT_PASSED_ERROR);
process.exit(1);
}
// check if initial users password is passed from the env or CLI
if (!password) {
console.error(errorMessage.INITIAL_USER_PASSWORD_NOT_PASSED_ERROR);
process.exit(1);
}
// check if the usernmae is taken
const users = authService.getUsersByUsername({ username });
if (users.length > 0) {
console.error(errorMessage.USERNAME_TAKEN_ERROR);
process.exit(1);
}
// check if the password is strong
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password),
)
) {
console.error(errorMessage.WEAK_PASSWORD_ERROR);
process.exit(1);
}
// hash the password
const { hashedPassword, salt } = await hashPassword(
password,
SALT_ROUNDS,
);
// create the initial user
const { lastInsertRowid: userId } = rowService.save({
tableName: USERS_TABLE,
fields: {
username,
hashed_password: hashedPassword,
salt,
is_superuser: 'false',
},
});
// get the default role from the DB
const roles = authService.getDefaultRole();
if (roles.length <= 0) {
console.log(errorMessage.DEFAULT_ROLE_NOT_CREATED_ERROR);
process.exit(1);
}
const defaultRoleId = roles[0].id;
// create a _users_role for the initial user
rowService.save({
tableName: USERS_ROLES_TABLE,
fields: { user_id: userId, role_id: defaultRoleId },
});
console.log(successMessage.INITIAL_USER_CREATED_SUCCESS);
} else {
console.log(infoMessage.INITIAL_USER_ALREADY_CREATED);
}
} catch (error) {
console.log(error);
}
};
module.exports = {
updateSuperuser,
registerUser,
changePassword,
createInitialUser,
};

View File

@@ -1,7 +1,11 @@
const { dbConstants } = require('../constants');
const { tableFields, ROLES_TABLE, USERS_TABLE } = dbConstants;
module.exports = {
roleSchema: [
{
name: 'name',
name: tableFields.ROLE_NAME,
type: 'TEXT',
primaryKey: false,
notNull: true,
@@ -11,21 +15,21 @@ module.exports = {
userSchema: [
{
name: 'username',
name: tableFields.USERNAME,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: true,
},
{
name: 'hashed_password',
name: tableFields.HASHED_PASSWORD,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: true,
unique: false,
},
{
name: 'salt',
name: tableFields.SALT,
type: 'TEXT',
primaryKey: false,
notNull: true,
@@ -33,7 +37,7 @@ module.exports = {
},
{
name: 'is_superuser',
name: tableFields.IS_SUPERUSER,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
@@ -43,16 +47,16 @@ module.exports = {
rolePermissionSchema: [
{
name: 'role_id',
name: tableFields.ROLE_ID,
type: 'NUMERIC',
primaryKey: false,
notNull: true,
unique: false,
foreignKey: { table: '_roles', column: 'id' },
foreignKey: { table: ROLES_TABLE, column: tableFields.ID },
},
{
name: 'table_name',
name: tableFields.TABLE_NAME,
type: 'TEXT',
primaryKey: false,
notNull: true,
@@ -60,7 +64,7 @@ module.exports = {
},
{
name: 'create',
name: tableFields.CREATE,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
@@ -68,7 +72,7 @@ module.exports = {
},
{
name: 'read',
name: tableFields.READ,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
@@ -76,7 +80,7 @@ module.exports = {
},
{
name: 'update',
name: tableFields.UPDATE,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
@@ -84,7 +88,7 @@ module.exports = {
},
{
name: 'delete',
name: tableFields.DELETE,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
@@ -94,21 +98,21 @@ module.exports = {
usersRoleSchema: [
{
name: 'user_id',
name: tableFields.USER_ID,
type: 'NUMERIC',
primaryKey: false,
notNull: true,
unique: false,
foreignKey: { table: '_users', column: 'id' },
foreignKey: { table: USERS_TABLE, column: tableFields.ID },
},
{
name: 'role_id',
name: tableFields.ROLE_ID,
type: 'NUMERIC',
primaryKey: false,
notNull: true,
unique: false,
foreignKey: { table: '_roles', column: 'id' },
foreignKey: { table: ROLES_TABLE, column: tableFields.ID },
},
],
};

View File

@@ -1,44 +1,42 @@
const config = require('../config');
const { registerUser, isUsernameTaken } = require('../controllers/auth');
const { apiConstants, dbConstants } = require('../constants/');
const { registerUser } = require('../controllers/auth');
const {
apiConstants,
dbConstants,
responseMessages,
} = require('../constants/');
const { removeFields } = require('../utils');
const { SALT, HASHED_PASSWORD, IS_SUPERUSER } = apiConstants.fields._users;
const { reservedTableNames } = dbConstants;
const { httpVerbs } = apiConstants;
const { reservedTableNames, USERS_TABLE, tableFields } = dbConstants;
const { errorMessage } = responseMessages;
const processRowRequest = async (req, res, next) => {
const resource = req.params.name;
const { method, body } = req;
const fields = body.fields;
const { method } = req;
// If the user sends a request when auth is set to false, throw an error
if (apiConstants.defaultRoutes.includes(resource) && !config.auth) {
// If the user sends a request to the auth tables while AUTH is set to false, throw an error
if (apiConstants.authEndpoints.includes(resource) && !config.auth) {
return res.status(403).send({
message: 'You can not access this endpoint while AUTH is set to false',
message: errorMessage.AUTH_SET_TO_FALSE_ERROR,
});
}
// Redirect this request to the registerUser controller => POST /api/tables/_users/rows
if (resource === '_users' && method === 'POST') {
if (resource === USERS_TABLE && method === httpVerbs.POST) {
return registerUser(req, res);
}
// Remove some fields for this request and check the username field => PUT /api/tables/_users/rows
if (resource === '_users' && 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' });
}
}
if (resource === USERS_TABLE && method === httpVerbs.PUT) {
/**
* remove some user fields from the request like (is_superuser, hashed_password, salt).
* NOTE: password can be updated via the /change-password API and superuser status can be only updated from the CLI
*/
removeFields([req.body.fields], [SALT, IS_SUPERUSER, HASHED_PASSWORD]);
removeFields(
[req.body.fields],
[tableFields.SALT, tableFields.IS_SUPERUSER, tableFields.HASHED_PASSWORD],
);
}
next();
@@ -51,8 +49,8 @@ const processRowResponse = async (req, res, next) => {
const payload = req.response.payload;
// Remove some fields from the response
if (resource === '_users') {
removeFields(payload.data, [SALT, HASHED_PASSWORD]);
if (resource === USERS_TABLE) {
removeFields(payload.data, [tableFields.SALT, tableFields.HASHED_PASSWORD]);
}
res.status(status).send(payload);
@@ -63,10 +61,10 @@ const processTableRequest = async (req, res, next) => {
const { method, body, baseUrl } = req;
// if the user tries to create a table with the reserved table names throw an error. Request => POST /api/tables
if (baseUrl === apiConstants.baseTableUrl && method === 'POST') {
if (baseUrl === apiConstants.baseTableUrl && method === httpVerbs.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}.`,
message: errorMessage.RESERVED_TABLE_NAME_ERROR,
});
}
}

View File

@@ -1,8 +1,10 @@
const config = require('../config');
const { decodeToken, toBoolean } = require('../utils/index');
const { apiConstants } = require('../constants');
const { apiConstants, responseMessages } = require('../constants');
const isAuthenticated = async (req, res, next) => {
const { errorMessage } = responseMessages;
const hasAccess = async (req, res, next) => {
let payload;
const { name: tableName } = req.params;
const verb = req.method;
@@ -18,7 +20,9 @@ const isAuthenticated = async (req, res, next) => {
);
req.user = payload;
} catch (error) {
return res.status(403).send({ message: 'Invalid access token' });
return res
.status(401)
.send({ message: errorMessage.INVALID_ACCESS_TOKEN_ERROR });
}
// if the user is a super_user, allow access on the resource
@@ -33,7 +37,9 @@ const isAuthenticated = async (req, res, next) => {
// if table_name is not passed from the router throw unauthorized error
if (!tableName) {
return res.status(403).send({ message: 'Not authorized' });
return res
.status(403)
.send({ message: errorMessage.NOT_AUTHORIZED_ERROR });
}
// if the user is not a super user, check the users permission on the resource
@@ -44,14 +50,15 @@ const isAuthenticated = async (req, res, next) => {
if (permissions.length <= 0) {
return res
.status(403)
.send({ message: 'Permission not defined for this role' });
.send({ message: errorMessage.PERMISSION_NOT_DEFINED_ERROR });
}
// If the user has permission on the table in at least in one of the roles then allow access on the table
let hasPermission = false;
permissions.some((resource) => {
const httpMethod = apiConstants.httpVerbs[verb].toLowerCase();
const httpMethod =
apiConstants.httpMethodDefinitions[verb].toLowerCase();
if (toBoolean(resource[httpMethod])) {
hasPermission = true;
@@ -62,7 +69,9 @@ const isAuthenticated = async (req, res, next) => {
if (hasPermission) {
next();
} else {
return res.status(403).send({ message: 'Not authorized' });
return res
.status(403)
.send({ message: errorMessage.NOT_AUTHORIZED_ERROR });
}
} else {
next();
@@ -73,4 +82,4 @@ const isAuthenticated = async (req, res, next) => {
}
};
module.exports = { isAuthenticated };
module.exports = { hasAccess };

View File

@@ -1,6 +1,6 @@
const validator = (schema) => (req, res, next) => {
const { body, params, query } = req;
const data = { body, params, query };
const { body, params, query, cookies } = req;
const data = { body, params, query, cookies };
const { value, error } = schema.validate(data);
@@ -13,6 +13,7 @@ const validator = (schema) => (req, res, next) => {
req.body = value.body;
req.params = value.params;
req.query = value.query;
req.cookies = value.cookies;
next();
}

View File

@@ -3,7 +3,7 @@ const express = require('express');
const controllers = require('../controllers/auth');
const { validator } = require('../middlewares/validation');
const schema = require('../schemas/auth');
const { isAuthenticated } = require('../middlewares/auth');
const { hasAccess } = require('../middlewares/auth');
const router = express.Router();
@@ -21,8 +21,8 @@ router.get(
router.put(
'/change-password',
hasAccess,
validator(schema.changePassword),
isAuthenticated,
controllers.changePassword,
);

View File

@@ -4,14 +4,14 @@ const controllers = require('../controllers/rows');
const { broadcast } = require('../middlewares/broadcast');
const { validator } = require('../middlewares/validation');
const { processRowRequest, processRowResponse } = require('../middlewares/api');
const { isAuthenticated } = require('../middlewares/auth');
const { hasAccess } = require('../middlewares/auth');
const schema = require('../schemas/rows');
const router = express.Router();
router.get(
'/:name/rows',
isAuthenticated,
hasAccess,
validator(schema.listTableRows),
processRowRequest,
controllers.listTableRows,
@@ -19,7 +19,7 @@ router.get(
);
router.post(
'/:name/rows',
isAuthenticated,
hasAccess,
validator(schema.insertRowInTable),
processRowRequest,
controllers.insertRowInTable,
@@ -27,14 +27,14 @@ router.post(
);
router.get(
'/:name/rows/:pks',
isAuthenticated,
hasAccess,
validator(schema.getRowInTableByPK),
controllers.getRowInTableByPK,
processRowResponse,
);
router.put(
'/:name/rows/:pks',
isAuthenticated,
hasAccess,
validator(schema.updateRowInTableByPK),
processRowRequest,
controllers.updateRowInTableByPK,
@@ -42,7 +42,7 @@ router.put(
);
router.delete(
'/:name/rows/:pks',
isAuthenticated,
hasAccess,
validator(schema.deleteRowInTableByPK),
controllers.deleteRowInTableByPK,
broadcast,

View File

@@ -3,14 +3,14 @@ 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 { hasAccess } = require('../middlewares/auth');
const { processTableRequest } = require('../middlewares/api');
const router = express.Router();
router.get(
'/',
isAuthenticated,
hasAccess,
validator(schema.listTables),
controllers.listTables,
);
@@ -18,21 +18,21 @@ router.get(
router.post(
'/',
processTableRequest,
isAuthenticated,
hasAccess,
validator(schema.createTable),
controllers.createTable,
);
router.get(
'/:name',
isAuthenticated,
hasAccess,
validator(schema.getTableSchema),
controllers.getTableSchema,
);
router.delete(
'/:name',
isAuthenticated,
hasAccess,
validator(schema.deleteTable),
controllers.deleteTable,
);

View File

@@ -10,12 +10,21 @@ const obtainAccessToken = Joi.object({
password: Joi.string().required(),
}).required(),
}).required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
const refreshAccessToken = Joi.object({
query: Joi.object().required(),
params: Joi.object({}).required(),
body: Joi.object({}).required(),
cookies: Joi.object({
refreshToken: Joi.string().required(),
accessToken: Joi.string().optional(),
}).required(),
});
const changePassword = Joi.object({
@@ -28,10 +37,31 @@ const changePassword = Joi.object({
newPassword: Joi.string().required(),
}).required(),
}).required(),
cookies: Joi.object({
accessToken: Joi.string().required(),
refreshToken: Joi.string().optional(),
}).required(),
});
const registerUser = Joi.object({
query: Joi.object().required(),
params: Joi.object({}).required(),
body: Joi.object({
fields: Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}).required(),
}).required(),
cookies: Joi.object({
accessToken: Joi.string().required(),
refreshToken: Joi.string().optional(),
}).required(),
});
module.exports = {
obtainAccessToken,
refreshAccessToken,
changePassword,
registerUser,
};

View File

@@ -12,10 +12,15 @@ const transaction = Joi.object({
}),
Joi.object({
query: Joi.string().required(),
})
}),
)
.required(),
}).required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
module.exports = {

View File

@@ -14,6 +14,10 @@ const listTableRows = Joi.object({
name: Joi.string(),
}).required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
const insertRowInTable = Joi.object({
@@ -28,6 +32,10 @@ const insertRowInTable = Joi.object({
body: Joi.object({
fields: Joi.object().required(),
}).required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
const getRowInTableByPK = Joi.object({
@@ -48,6 +56,10 @@ const getRowInTableByPK = Joi.object({
pks: Joi.string().required(),
}).required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
const updateRowInTableByPK = Joi.object({
@@ -68,6 +80,10 @@ const updateRowInTableByPK = Joi.object({
body: Joi.object({
fields: Joi.object().required(),
}).required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
const deleteRowInTableByPK = Joi.object({
@@ -86,6 +102,10 @@ const deleteRowInTableByPK = Joi.object({
pks: Joi.string().required(),
}).required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
module.exports = {

View File

@@ -7,6 +7,10 @@ const listTables = Joi.object({
}).required(),
params: Joi.object().required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
const createTable = Joi.object({
@@ -38,7 +42,7 @@ const createTable = Joi.object({
'BLOB',
'BOOLEAN',
'DATE',
'DATETIME'
'DATETIME',
)
.insensitive()
.required(),
@@ -67,10 +71,14 @@ const createTable = Joi.object({
.default('RESTRICT'),
}),
index: Joi.boolean(),
})
}),
)
.required(),
}),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
const getTableSchema = Joi.object({
@@ -83,6 +91,10 @@ const getTableSchema = Joi.object({
.required(),
}),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
const deleteTable = Joi.object({
@@ -95,6 +107,10 @@ const deleteTable = Joi.object({
.required(),
}),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
module.exports = {

View File

@@ -0,0 +1,78 @@
const db = require('../db');
const rowService = require('./rowService')(db);
const { constantRoles, dbConstants } = require('../constants');
const {
USERS_TABLE,
ROLES_TABLE,
USERS_ROLES_TABLE,
ROLES_PERMISSIONS_TABLE,
tableFields,
} = dbConstants;
module.exports = () => {
return {
getUsersByUsername({ username }) {
const users = rowService.get({
tableName: USERS_TABLE,
whereString: `WHERE ${tableFields.USERNAME} =?`,
whereStringValues: [username],
});
return users;
},
getUsersById({ userId }) {
const users = rowService.get({
tableName: USERS_TABLE,
whereString: `WHERE ${tableFields.ID}=?`,
whereStringValues: [userId],
});
return users;
},
getAllUsers() {
const users = rowService.get({
tableName: USERS_TABLE,
whereString: '',
whereStringValues: [],
});
return users;
},
getPermissionByRoleIds({ roleIds }) {
const permissions = rowService.get({
tableName: ROLES_PERMISSIONS_TABLE,
whereString: `WHERE ${tableFields.ROLE_ID} IN (${roleIds.map(
() => '?',
)})`,
whereStringValues: [...roleIds],
});
return permissions;
},
getUserRoleByUserId({ userId }) {
const userRoles = rowService.get({
tableName: USERS_ROLES_TABLE,
whereString: `WHERE ${tableFields.USER_ID} =?`,
whereStringValues: [userId],
});
return userRoles;
},
getDefaultRole() {
const defaultRole = rowService.get({
tableName: ROLES_TABLE,
whereString: `WHERE ${tableFields.ROLE_NAME}=?`,
whereStringValues: [constantRoles.DEFAULT_ROLE],
});
return defaultRole;
},
};
};

View File

@@ -2,5 +2,6 @@ const db = require('../db');
const rowService = require('./rowService')(db);
const tableService = require('./tableService')(db);
const authService = require('./authService')(db);
module.exports = { rowService, tableService };
module.exports = { rowService, tableService, authService };

View File

@@ -278,9 +278,6 @@
},
"403": {
"description": "Forbidden"
},
"409": {
"description": "Conflict"
}
}
},
@@ -323,9 +320,6 @@
},
"403": {
"description": "Forbidden"
},
"409": {
"description": "Conflict"
}
}
}
@@ -454,9 +448,6 @@
},
"403": {
"description": "Forbidden"
},
"409": {
"description": "Conflict"
}
}
},
@@ -508,111 +499,39 @@
},
"/api/auth/token/obtain": {
"post": {
"tags": ["Auth"],
"summary": "Obtain Access Token",
"description": "Endpoint to generate access and refresh tokens",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/ObtainAccessTokenRequestBody"
}
}
],
"description": "",
"parameters": [],
"responses": {
"201": {
"description": "Access token and Refresh token generated",
"schema": {
"$ref": "#/definitions/ObtainAccessTokenSuccessResponse"
}
},
"400": {
"description": "Bad Request"
},
"401": {
"description": "Invalid username or password error",
"schema": {
"$ref": "#/definitions/InvalidCredentialErrorResponse"
}
},
"404": {
"description": "Not Found"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/auth/token/refresh": {
"get": {
"tags": ["Auth"],
"summary": "Refresh Access Token",
"description": "Endpoint to refresh access and refresh tokens",
"description": "",
"parameters": [],
"responses": {
"200": {
"description": "Access token refreshed",
"schema": {
"$ref": "#/definitions/RefreshAccessTokenSuccessResponse"
}
},
"400": {
"description": "Bad Request"
},
"401": {
"description": "Invalid refresh token error",
"schema": {
"$ref": "#/definitions/InvalidRefreshTokenErrorResponse"
}
},
"403": {
"description": "Forbidden"
}
}
}
},
"/api/auth/change-password": {
"put": {
"tags": ["Auth"],
"summary": "Change Password",
"description": "Endpoint to change a password",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/ChangePasswordRequestBody"
}
}
],
"description": "",
"parameters": [],
"responses": {
"200": {
"description": "Weak password error",
"schema": {
"$ref": "#/definitions/ChangePasswordSuccessResponse"
}
},
"400": {
"description": "Weak password error",
"schema": {
"$ref": "#/definitions/WeakPasswordErrorResponse"
}
"description": "Bad Request"
},
"401": {
"description": "User not found error",
"schema": {
"$ref": "#/definitions/InvalidPasswordErrorResponse"
}
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"500": {
"description": "Internal Server Error"
}
}
}