From 2e69bfb304e8dd5ab22360db758b3fe3a3e81b78 Mon Sep 17 00:00:00 2001 From: AbegaM Date: Mon, 18 Mar 2024 17:54:15 +0300 Subject: [PATCH] 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 --- .vscode/settings.json | 0 src/constants/api.js | 17 +- src/constants/auth.js | 5 + src/constants/index.js | 10 +- src/constants/messages.js | 41 ++ src/constants/tables.js | 50 ++- src/controllers/auth.js | 729 --------------------------------- src/controllers/auth.test.js | 24 -- src/controllers/auth/common.js | 16 + src/controllers/auth/index.js | 5 + src/controllers/auth/tables.js | 96 +++++ src/controllers/auth/token.js | 243 +++++++++++ src/controllers/auth/user.js | 394 ++++++++++++++++++ src/db/schema.js | 38 +- src/index.js | 2 +- src/middlewares/api.js | 48 ++- src/middlewares/auth.js | 25 +- src/middlewares/validation.js | 5 +- src/routes/auth.js | 4 +- src/routes/rows.js | 12 +- src/routes/tables.js | 10 +- src/schemas/auth.js | 30 ++ src/schemas/index.js | 7 +- src/schemas/rows.js | 20 + src/schemas/tables.js | 20 +- src/services/authService.js | 78 ++++ src/services/index.js | 3 +- src/swagger/swagger.json | 95 +---- 28 files changed, 1099 insertions(+), 928 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 src/constants/auth.js create mode 100644 src/constants/messages.js delete mode 100644 src/controllers/auth.js create mode 100644 src/controllers/auth/common.js create mode 100644 src/controllers/auth/index.js create mode 100644 src/controllers/auth/tables.js create mode 100644 src/controllers/auth/token.js create mode 100644 src/controllers/auth/user.js create mode 100644 src/services/authService.js diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e69de29..0000000 diff --git a/src/constants/api.js b/src/constants/api.js index b898b09..d32d917 100644 --- a/src/constants/api.js +++ b/src/constants/api.js @@ -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', diff --git a/src/constants/auth.js b/src/constants/auth.js new file mode 100644 index 0000000..2c18c55 --- /dev/null +++ b/src/constants/auth.js @@ -0,0 +1,5 @@ +module.exports = { + SALT_ROUNDS: 10, + ACCESS_TOKEN_SUBJECT: 'accessToken', + REFRESH_TOKEN_SUBJECT: 'refreshToken', +}; diff --git a/src/constants/index.js b/src/constants/index.js index 4f1d339..d89805c 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -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, +}; diff --git a/src/constants/messages.js b/src/constants/messages.js new file mode 100644 index 0000000..0380680 --- /dev/null +++ b/src/constants/messages.js @@ -0,0 +1,41 @@ +module.exports = { + successMessage: { + SUCCESS: 'Success', + ROW_INSERTED: 'Row Inserted', + PASSWORD_UPDATE_SUCCESS: 'Password updated successfully', + USER_UPDATE_SUCCESS: 'User updated successfully', + INITIAL_USER_CREATED_SUCCESS: 'Initial user created successfully', + }, + + errorMessage: { + USERNAME_TAKEN_ERROR: 'This username is taken', + WEAK_PASSWORD_ERROR: 'This password is weak, please use another password', + DEFAULT_ROLE_NOT_CREATED_ERROR: + 'Please restart soul so a default role can be created', + INVALID_USERNAME_PASSWORD_ERROR: 'Invalid username or password', + INVALID_REFRESH_TOKEN_ERROR: 'Invalid refresh token', + INVALID_ACCESS_TOKEN_ERROR: 'Invalid access token', + USER_NOT_FOUND_ERROR: 'User not found', + INVALID_CURRENT_PASSWORD_ERROR: 'Invalid current password', + NOT_AUTHORIZED_ERROR: 'Not authorized', + PERMISSION_NOT_DEFINED_ERROR: 'Permission not defined for this role', + ROLE_NOT_FOUND_ERROR: 'Role not found for this user', + AUTH_SET_TO_FALSE_ERROR: + 'You can not access this endpoint while AUTH is set to false', + RESERVED_TABLE_NAME_ERROR: + 'The table name is reserved. Please choose a different name for the table.', + SERVER_ERROR: 'Server error', + + INITIAL_USER_USERNAME_NOT_PASSED_ERROR: + 'Error: You should pass the initial users username either from the CLI with the --iuu or from the environment variable using the INITIAL_USER_USERNAME flag', + INITIAL_USER_PASSWORD_NOT_PASSED_ERROR: + 'Error: You should pass the initial users password either from the CLI with the --iup or from the environment variable using the INITIAL_USER_PASSWORD flag', + + USERNAME_REQUIRED_ERROR: 'username is required', + PASSWORD_REQUIRED_ERROR: 'password is required', + }, + + infoMessage: { + INITIAL_USER_ALREADY_CREATED: 'Initial user is already created', + }, +}; diff --git a/src/constants/tables.js b/src/constants/tables.js index dee2a99..d2c1772 100644 --- a/src/constants/tables.js +++ b/src/constants/tables.js @@ -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', + }, }; diff --git a/src/controllers/auth.js b/src/controllers/auth.js deleted file mode 100644 index c8c6be0..0000000 --- a/src/controllers/auth.js +++ /dev/null @@ -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, -}; diff --git a/src/controllers/auth.test.js b/src/controllers/auth.test.js index ef473d9..ec38345 100644 --- a/src/controllers/auth.test.js +++ b/src/controllers/auth.test.js @@ -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 }, diff --git a/src/controllers/auth/common.js b/src/controllers/auth/common.js new file mode 100644 index 0000000..777930d --- /dev/null +++ b/src/controllers/auth/common.js @@ -0,0 +1,16 @@ +const { rowService } = require('../../services'); +const { dbConstants } = require('../../constants'); + +const { USERS_TABLE } = dbConstants; + +const isUsernameTaken = (username) => { + const users = rowService.get({ + tableName: USERS_TABLE, + whereString: 'WHERE username=?', + whereStringValues: [username], + }); + + return users.length > 0; +}; + +module.exports = { isUsernameTaken }; diff --git a/src/controllers/auth/index.js b/src/controllers/auth/index.js new file mode 100644 index 0000000..d7e3c44 --- /dev/null +++ b/src/controllers/auth/index.js @@ -0,0 +1,5 @@ +const users = require('./user'); +const token = require('./token'); +const tables = require('./tables'); + +module.exports = { ...users, ...token, ...tables }; diff --git a/src/controllers/auth/tables.js b/src/controllers/auth/tables.js new file mode 100644 index 0000000..0dbdab0 --- /dev/null +++ b/src/controllers/auth/tables.js @@ -0,0 +1,96 @@ +const { tableService, rowService } = require('../../services'); +const { constantRoles, dbConstants } = require('../../constants'); +const schema = require('../../db/schema'); + +const { + USERS_TABLE, + ROLES_TABLE, + USERS_ROLES_TABLE, + ROLES_PERMISSIONS_TABLE, + constraints, + tableFields, +} = dbConstants; + +const createDefaultTables = async () => { + let roleId; + + // check if the default tables are already created + const roleTable = tableService.checkTableExists(ROLES_TABLE); + const usersTable = tableService.checkTableExists(USERS_TABLE); + const rolesPermissionTable = tableService.checkTableExists( + ROLES_PERMISSIONS_TABLE, + ); + const usersRolesTable = tableService.checkTableExists(USERS_ROLES_TABLE); + + // create _users table + if (!usersTable) { + tableService.createTable(USERS_TABLE, schema.userSchema); + } + + // create _users_roles table + if (!usersRolesTable) { + tableService.createTable( + USERS_ROLES_TABLE, + + schema.usersRoleSchema, + { + multipleUniqueConstraints: { + name: constraints.UNIQUE_USERS_ROLE, + fields: [tableFields.USER_ID, tableFields.USER_ID], + }, + }, + ); + } + + // create _roles table + if (!roleTable) { + tableService.createTable(ROLES_TABLE, schema.roleSchema); + + // create a default role in the _roles table + const role = rowService.save({ + tableName: ROLES_TABLE, + fields: { name: constantRoles.DEFAULT_ROLE }, + }); + roleId = role.lastInsertRowid; + } + + // create _roles_permissions table + if (!rolesPermissionTable && roleId) { + tableService.createTable( + ROLES_PERMISSIONS_TABLE, + schema.rolePermissionSchema, + { + multipleUniqueConstraints: { + name: constraints.UNIQUE_ROLES_TABLE, + fields: [tableFields.ROLE_ID, tableFields.TABLE_NAME], + }, + }, + ); + + // fetch all DB tables + const tables = tableService.listTables(); + + // add permission for the default role (for each db table) + const permissions = []; + for (const table of tables) { + permissions.push({ + role_id: roleId, + table_name: table.name, + create: 'false', + read: 'true', + update: 'false', + delete: 'false', + }); + } + + // store the permissions in the db + rowService.bulkWrite({ + tableName: ROLES_PERMISSIONS_TABLE, + fields: permissions, + }); + } +}; + +module.exports = { + createDefaultTables, +}; diff --git a/src/controllers/auth/token.js b/src/controllers/auth/token.js new file mode 100644 index 0000000..cd0e74f --- /dev/null +++ b/src/controllers/auth/token.js @@ -0,0 +1,243 @@ +const { authService } = require('../../services'); +const { responseMessages, authConstants } = require('../../constants'); +const config = require('../../config'); +const { + comparePasswords, + generateToken, + decodeToken, + toBoolean, +} = require('../../utils'); + +const { successMessage, errorMessage } = responseMessages; + +const obtainAccessToken = async (req, res) => { + /* + #swagger.tags = ['Auth'] + #swagger.summary = 'Obtain Access Token' + #swagger.description = 'Endpoint to generate access and refresh tokens' + + #swagger.parameters['body'] = { + in: 'body', + required: true, + type: 'object', + schema: { $ref: '#/definitions/ObtainAccessTokenRequestBody' } + } + */ + + // extract payload + const { username, password } = req.body.fields; + + try { + // check if the username exists in the Db + const users = authService.getUsersByUsername({ username }); + + if (users.length <= 0) { + return res + .status(401) + .send({ message: errorMessage.INVALID_USERNAME_PASSWORD_ERROR }); + } + + // check if the password is valid + const user = users[0]; + const isMatch = await comparePasswords(password, user.hashed_password); + + if (!isMatch) { + return res + .status(401) + .send({ message: errorMessage.INVALID_USERNAME_PASSWORD_ERROR }); + /* + #swagger.responses[401] = { + description: 'Invalid username or password error', + schema: { + $ref: '#/definitions/InvalidCredentialErrorResponse' + } + } + */ + } + + let permissions, roleIds; + + // if the user is not a superuser get the role and its permission from the DB + if (!toBoolean(user.is_superuser)) { + const roleData = getUsersRoleAndPermission({ + userId: user.id, + res, + }); + + permissions = roleData.permissions; + roleIds = roleData.roleIds; + } + + const payload = { + username: user.username, + userId: user.id, + isSuperuser: user.is_superuser, + roleIds, + permissions, + }; + + // generate an access token + const accessToken = await generateToken( + { subject: authConstants.ACCESS_TOKEN_SUBJECT, ...payload }, + config.tokenSecret, + config.accessTokenExpirationTime, + ); + + // generate a refresh token + const refreshToken = await generateToken( + { subject: authConstants.REFRESH_TOKEN_SUBJECT, ...payload }, + config.tokenSecret, + config.refreshTokenExpirationTime, + ); + + // set the token in the cookie + let cookieOptions = { httpOnly: true, secure: false, Path: '/' }; + res.cookie(authConstants.ACCESS_TOKEN_SUBJECT, accessToken, cookieOptions); + res.cookie( + authConstants.REFRESH_TOKEN_SUBJECT, + refreshToken, + cookieOptions, + ); + + res + .status(201) + .send({ message: successMessage.SUCCESS, data: { userId: user.id } }); + + /* + #swagger.responses[201] = { + description: 'Access token and Refresh token generated', + schema: { + $ref: '#/definitions/ObtainAccessTokenSuccessResponse' + } + } + */ + } catch (error) { + console.log(error); + return res.status(500).json({ + message: errorMessage.SERVER_ERROR, + }); + } +}; + +const refreshAccessToken = async (req, res) => { + /* + #swagger.tags = ['Auth'] + #swagger.summary = 'Refresh Access Token' + #swagger.description = 'Endpoint to refresh access and refresh tokens' + */ + + try { + // extract the payload from the token and verify it + const payload = await decodeToken( + req.cookies.refreshToken, + config.tokenSecret, + ); + + // find the user + const users = authService.getUsersById({ userId: payload.userId }); + + if (users.length <= 0) { + return res + .status(401) + .send({ message: errorMessage.USER_NOT_FOUND_ERROR }); + + /* + #swagger.responses[401] = { + description: 'User not found error', + schema: { + $ref: '#/definitions/UserNotFoundErrorResponse' + } + } + */ + } + + let permissions, roleIds; + const user = users[0]; + + // if the user is not a superuser get the role and its permission from the DB + if (!toBoolean(user.is_superuser)) { + const roleData = getUsersRoleAndPermission({ + userId: user.id, + res, + }); + + permissions = roleData.permissions; + roleIds = roleData.roleIds; + } + + const newPayload = { + username: user.username, + userId: user.id, + isSuperuser: user.is_superuser, + roleIds, + permissions, + }; + + // generate an access token + const accessToken = await generateToken( + { subject: authConstants.ACCESS_TOKEN_SUBJECT, ...newPayload }, + config.tokenSecret, + config.accessTokenExpirationTime, + ); + + // generate a refresh token + const refreshToken = await generateToken( + { subject: authConstants.REFRESH_TOKEN_SUBJECT, ...newPayload }, + config.tokenSecret, + config.refreshTokenExpirationTime, + ); + + // set the token in the cookie + let cookieOptions = { httpOnly: true, secure: false, Path: '/' }; + res.cookie(authConstants.ACCESS_TOKEN_SUBJECT, accessToken, cookieOptions); + res.cookie( + authConstants.REFRESH_TOKEN_SUBJECT, + refreshToken, + cookieOptions, + ); + + res + .status(200) + .send({ message: successMessage.SUCCESS, data: { userId: user.id } }); + + /* + #swagger.responses[200] = { + description: 'Access token refreshed', + schema: { + $ref: '#/definitions/RefreshAccessTokenSuccessResponse' + } + } + */ + } catch (error) { + res.status(403).send({ message: errorMessage.INVALID_REFRESH_TOKEN_ERROR }); + /* + #swagger.responses[401] = { + description: 'Invalid refresh token error', + schema: { + $ref: '#/definitions/InvalidRefreshTokenErrorResponse' + } + } + */ + } +}; + +const getUsersRoleAndPermission = ({ userId, res }) => { + const userRoles = authService.getUserRoleByUserId({ userId }); + + if (userRoles <= 0) { + res.status(401).send({ message: errorMessage.ROLE_NOT_FOUND_ERROR }); + throw new Error(errorMessage.ROLE_NOT_FOUND_ERROR); + } + + const roleIds = userRoles.map((role) => role.role_id); + + // get the permission of the role + const permissions = authService.getPermissionByRoleIds({ roleIds }); + + return { userRoles, roleIds, permissions }; +}; + +module.exports = { + obtainAccessToken, + refreshAccessToken, +}; diff --git a/src/controllers/auth/user.js b/src/controllers/auth/user.js new file mode 100644 index 0000000..c7eccbc --- /dev/null +++ b/src/controllers/auth/user.js @@ -0,0 +1,394 @@ +const { rowService, authService } = require('../../services'); +const { + apiConstants, + dbConstants, + responseMessages, + authConstants, +} = require('../../constants'); +const config = require('../../config'); +const { + hashPassword, + checkPasswordStrength, + comparePasswords, +} = require('../../utils'); + +const { USERS_TABLE, USERS_ROLES_TABLE, tableFields } = dbConstants; + +const { SALT_ROUNDS } = authConstants; + +const { successMessage, errorMessage, infoMessage } = responseMessages; + +const updateSuperuser = async (fields) => { + const { id, password, is_superuser } = fields; + let newHashedPassword, newSalt; + let fieldsString = ''; + + try { + // find the user by using the id field + const users = authService.getRoleByUserId({ id }); + + // abort if the id is invalid + if (users.length === 0) { + console.log(errorMessage.USER_NOT_FOUND_ERROR); + process.exit(1); + } + + // check if the is_superuser field is passed + if (is_superuser !== undefined) { + fieldsString = `${tableFields.IS_SUPERUSER} = '${is_superuser}'`; + } + + // if the password is sent from the CLI, update it + if (password) { + // check if the password is weak + if ( + [apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes( + checkPasswordStrength(password), + ) + ) { + console.log(errorMessage.WEAK_PASSWORD_ERROR); + process.exit(1); + } + + //hash the password + const { hashedPassword, salt } = await hashPassword( + password, + SALT_ROUNDS, + ); + newHashedPassword = hashedPassword; + newSalt = salt; + + fieldsString = `${fieldsString ? fieldsString + ', ' : ''} ${ + tableFields.HASHED_PASSWORD + } = '${newHashedPassword}', ${tableFields.SALT} = '${newSalt}'`; + } + + // update the user + rowService.update({ + tableName: USERS_TABLE, + lookupField: tableFields.ID, + fieldsString, + pks: `${id}`, + }); + + console.log(successMessage.USER_UPDATE_SUCCESS); + process.exit(1); + } catch (error) { + console.log(error); + } +}; + +const registerUser = async (req, res) => { + /* + #swagger.tags = ['Auth'] + #swagger.summary = 'Register User' + #swagger.description = 'Endpoint to signup' + + #swagger.parameters['username'] = { + in: 'body', + required: true, + type: 'object', + schema: { $ref: '#/definitions/UserRegistrationRequestBody' } + } + */ + + const { username, password } = req.body.fields; + + try { + if (!username) { + return res + .status(400) + .send({ message: errorMessage.USERNAME_REQUIRED_ERROR }); + } + + if (!password) { + return res + .status(400) + .send({ message: errorMessage.PASSWORD_REQUIRED_ERROR }); + } + + // check if the username is taken + const users = authService.getUsersByUsername({ username }); + + if (users.length > 0) { + return res + .status(409) + .send({ message: errorMessage.USERNAME_TAKEN_ERROR }); + + /* + #swagger.responses[409] = { + description: 'Username taken error', + schema: { + $ref: '#/definitions/UsernameTakenErrorResponse' + } + } + */ + } + + // check if the password is weak + if ( + [apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes( + checkPasswordStrength(password), + ) + ) { + return res.status(400).send({ + message: errorMessage.WEAK_PASSWORD_ERROR, + }); + + /* + #swagger.responses[400] = { + description: 'Weak password error', + schema: { + $ref: '#/definitions/WeakPasswordErrorResponse' + } + } + */ + } + + // hash the password + const { salt, hashedPassword } = await hashPassword(password, SALT_ROUNDS); + + // create the user + const newUser = rowService.save({ + tableName: USERS_TABLE, + fields: { + username, + salt, + hashed_password: hashedPassword, + is_superuser: 'false', + }, + }); + + // find the default role from the DB + const defaultRole = authService.getDefaultRole(); + + if (defaultRole.length <= 0) { + return res.status(500).send({ + message: errorMessage.DEFAULT_ROLE_NOT_CREATED_ERROR, + }); + /* + #swagger.responses[500] = { + description: 'Server error', + schema: { + $ref: '#/definitions/DefaultRoleNotCreatedErrorResponse' + } + } + */ + } + + // create a role for the user + rowService.save({ + tableName: USERS_ROLES_TABLE, + fields: { user_id: newUser.lastInsertRowid, role_id: defaultRole[0].id }, + }); + + res.status(201).send({ message: successMessage.ROW_INSERTED }); + + /* + #swagger.responses[201] = { + description: 'Row inserted', + schema: { + $ref: '#/definitions/InsertRowSuccessResponse' + } + } + */ + } catch (error) { + console.log(error); + res.status(500).send({ message: errorMessage.SERVER_ERROR }); + } +}; + +const changePassword = async (req, res) => { + /* + #swagger.tags = ['Auth'] + #swagger.summary = 'Change Password' + #swagger.description = 'Endpoint to change a password' + + #swagger.parameters['body'] = { + in: 'body', + required: true, + type: 'object', + schema: { + $ref: '#/definitions/ChangePasswordRequestBody' + } + } + */ + + const userInfo = req.user; + const { currentPassword, newPassword } = req.body.fields; + + try { + // get the user from the Db + const users = authService.getUsersById({ userId: userInfo.userId }); + + if (users.length <= 0) { + return res + .status(401) + .send({ message: errorMessage.USER_NOT_FOUND_ERROR }); + } + + const user = users[0]; + + // check if the users current password is valid + const isMatch = await comparePasswords( + currentPassword, + user.hashed_password, + ); + + if (!isMatch) { + return res + .status(401) + .send({ message: errorMessage.INVALID_CURRENT_PASSWORD_ERROR }); + /* + #swagger.responses[401] = { + description: 'User not found error', + schema: { + $ref: '#/definitions/InvalidPasswordErrorResponse' + } + } + */ + } + + // check if the new password is strong + if ( + [apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes( + checkPasswordStrength(newPassword), + ) + ) { + return res.status(400).send({ + message: errorMessage.WEAK_PASSWORD_ERROR, + }); + + /* + #swagger.responses[400] = { + description: 'Weak password error', + schema: { + $ref: '#/definitions/WeakPasswordErrorResponse' + } + } + */ + } + + // hash the password + const { salt, hashedPassword } = await hashPassword( + newPassword, + SALT_ROUNDS, + ); + + user.salt = salt; + user.hashed_password = hashedPassword; + + // update the user + rowService.update({ + tableName: USERS_TABLE, + lookupField: tableFields.ID, + fieldsString: `${tableFields.HASHED_PASSWORD} = '${hashedPassword}', ${tableFields.SALT} = '${salt}'`, + pks: `${user.id}`, + }); + + res.status(200).send({ + message: successMessage.PASSWORD_UPDATE_SUCCESS, + data: { id: user.id, username: user.username }, + }); + + /* + #swagger.responses[200] = { + description: 'Weak password error', + schema: { + $ref: '#/definitions/ChangePasswordSuccessResponse' + } + } + */ + } catch (error) { + res.status(500).send({ message: errorMessage.SERVER_ERROR }); + } +}; + +const createInitialUser = async () => { + // extract some fields from the environment variables or from the CLI + const { initialUserUsername: username, initialUserPassword: password } = + config; + + try { + // check if there are users in the DB + const users = authService.getAllUsers(); + + if (users.length <= 0) { + // check if initial users username is passed from the env or CLI + if (!username) { + console.error(errorMessage.INITIAL_USER_USERNAME_NOT_PASSED_ERROR); + process.exit(1); + } + + // check if initial users password is passed from the env or CLI + if (!password) { + console.error(errorMessage.INITIAL_USER_PASSWORD_NOT_PASSED_ERROR); + process.exit(1); + } + + // check if the usernmae is taken + const users = authService.getUsersByUsername({ username }); + + if (users.length > 0) { + console.error(errorMessage.USERNAME_TAKEN_ERROR); + process.exit(1); + } + + // check if the password is strong + if ( + [apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes( + checkPasswordStrength(password), + ) + ) { + console.error(errorMessage.WEAK_PASSWORD_ERROR); + process.exit(1); + } + + // hash the password + const { hashedPassword, salt } = await hashPassword( + password, + SALT_ROUNDS, + ); + + // create the initial user + const { lastInsertRowid: userId } = rowService.save({ + tableName: USERS_TABLE, + fields: { + username, + hashed_password: hashedPassword, + salt, + is_superuser: 'false', + }, + }); + + // get the default role from the DB + const roles = authService.getDefaultRole(); + + if (roles.length <= 0) { + console.log(errorMessage.DEFAULT_ROLE_NOT_CREATED_ERROR); + process.exit(1); + } + + const defaultRoleId = roles[0].id; + + // create a _users_role for the initial user + rowService.save({ + tableName: USERS_ROLES_TABLE, + fields: { user_id: userId, role_id: defaultRoleId }, + }); + + console.log(successMessage.INITIAL_USER_CREATED_SUCCESS); + } else { + console.log(infoMessage.INITIAL_USER_ALREADY_CREATED); + } + } catch (error) { + console.log(error); + } +}; + +module.exports = { + updateSuperuser, + registerUser, + changePassword, + createInitialUser, +}; diff --git a/src/db/schema.js b/src/db/schema.js index 64daaa0..2b51f88 100644 --- a/src/db/schema.js +++ b/src/db/schema.js @@ -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 }, }, ], }; diff --git a/src/index.js b/src/index.js index d4e0295..efcc9af 100755 --- a/src/index.js +++ b/src/index.js @@ -79,7 +79,7 @@ if (config.rateLimit.enabled) { app.use(limiter); } -//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 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(); diff --git a/src/middlewares/api.js b/src/middlewares/api.js index 707aa34..4b47a9a 100644 --- a/src/middlewares/api.js +++ b/src/middlewares/api.js @@ -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, }); } } diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js index a73875a..ec74577 100644 --- a/src/middlewares/auth.js +++ b/src/middlewares/auth.js @@ -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 }; diff --git a/src/middlewares/validation.js b/src/middlewares/validation.js index df18fdd..c8bcd69 100644 --- a/src/middlewares/validation.js +++ b/src/middlewares/validation.js @@ -1,6 +1,6 @@ const validator = (schema) => (req, res, next) => { - const { body, params, query } = req; - const data = { body, params, query }; + const { body, params, query, cookies } = req; + const data = { body, params, query, cookies }; const { value, error } = schema.validate(data); @@ -13,6 +13,7 @@ const validator = (schema) => (req, res, next) => { req.body = value.body; req.params = value.params; req.query = value.query; + req.cookies = value.cookies; next(); } diff --git a/src/routes/auth.js b/src/routes/auth.js index c787674..d238938 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -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, ); diff --git a/src/routes/rows.js b/src/routes/rows.js index 8da4a4d..d5fcdc9 100644 --- a/src/routes/rows.js +++ b/src/routes/rows.js @@ -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, diff --git a/src/routes/tables.js b/src/routes/tables.js index 11ba003..e75dbdc 100644 --- a/src/routes/tables.js +++ b/src/routes/tables.js @@ -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, ); diff --git a/src/schemas/auth.js b/src/schemas/auth.js index 45f4362..369edf2 100644 --- a/src/schemas/auth.js +++ b/src/schemas/auth.js @@ -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, }; diff --git a/src/schemas/index.js b/src/schemas/index.js index 5673219..8ce3ec3 100644 --- a/src/schemas/index.js +++ b/src/schemas/index.js @@ -12,10 +12,15 @@ const transaction = Joi.object({ }), Joi.object({ query: Joi.string().required(), - }) + }), ) .required(), }).required(), + + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); module.exports = { diff --git a/src/schemas/rows.js b/src/schemas/rows.js index 48502f4..d0e953a 100644 --- a/src/schemas/rows.js +++ b/src/schemas/rows.js @@ -14,6 +14,10 @@ const listTableRows = Joi.object({ name: Joi.string(), }).required(), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const insertRowInTable = Joi.object({ @@ -28,6 +32,10 @@ const insertRowInTable = Joi.object({ body: Joi.object({ fields: Joi.object().required(), }).required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const getRowInTableByPK = Joi.object({ @@ -48,6 +56,10 @@ const getRowInTableByPK = Joi.object({ pks: Joi.string().required(), }).required(), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const updateRowInTableByPK = Joi.object({ @@ -68,6 +80,10 @@ const updateRowInTableByPK = Joi.object({ body: Joi.object({ fields: Joi.object().required(), }).required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const deleteRowInTableByPK = Joi.object({ @@ -86,6 +102,10 @@ const deleteRowInTableByPK = Joi.object({ pks: Joi.string().required(), }).required(), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); module.exports = { diff --git a/src/schemas/tables.js b/src/schemas/tables.js index d35d078..e4cb082 100644 --- a/src/schemas/tables.js +++ b/src/schemas/tables.js @@ -7,6 +7,10 @@ const listTables = Joi.object({ }).required(), params: Joi.object().required(), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const createTable = Joi.object({ @@ -38,7 +42,7 @@ const createTable = Joi.object({ 'BLOB', 'BOOLEAN', 'DATE', - 'DATETIME' + 'DATETIME', ) .insensitive() .required(), @@ -67,10 +71,14 @@ const createTable = Joi.object({ .default('RESTRICT'), }), index: Joi.boolean(), - }) + }), ) .required(), }), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const getTableSchema = Joi.object({ @@ -83,6 +91,10 @@ const getTableSchema = Joi.object({ .required(), }), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); const deleteTable = Joi.object({ @@ -95,6 +107,10 @@ const deleteTable = Joi.object({ .required(), }), body: Joi.object().required(), + cookies: Joi.object({ + refreshToken: Joi.string().optional(), + accessToken: Joi.string().optional(), + }), }); module.exports = { diff --git a/src/services/authService.js b/src/services/authService.js new file mode 100644 index 0000000..32603f6 --- /dev/null +++ b/src/services/authService.js @@ -0,0 +1,78 @@ +const db = require('../db'); +const rowService = require('./rowService')(db); + +const { constantRoles, dbConstants } = require('../constants'); + +const { + USERS_TABLE, + ROLES_TABLE, + USERS_ROLES_TABLE, + ROLES_PERMISSIONS_TABLE, + tableFields, +} = dbConstants; + +module.exports = () => { + return { + getUsersByUsername({ username }) { + const users = rowService.get({ + tableName: USERS_TABLE, + whereString: `WHERE ${tableFields.USERNAME} =?`, + whereStringValues: [username], + }); + + return users; + }, + + getUsersById({ userId }) { + const users = rowService.get({ + tableName: USERS_TABLE, + whereString: `WHERE ${tableFields.ID}=?`, + whereStringValues: [userId], + }); + + return users; + }, + + getAllUsers() { + const users = rowService.get({ + tableName: USERS_TABLE, + whereString: '', + whereStringValues: [], + }); + + return users; + }, + + getPermissionByRoleIds({ roleIds }) { + const permissions = rowService.get({ + tableName: ROLES_PERMISSIONS_TABLE, + whereString: `WHERE ${tableFields.ROLE_ID} IN (${roleIds.map( + () => '?', + )})`, + whereStringValues: [...roleIds], + }); + + return permissions; + }, + + getUserRoleByUserId({ userId }) { + const userRoles = rowService.get({ + tableName: USERS_ROLES_TABLE, + whereString: `WHERE ${tableFields.USER_ID} =?`, + whereStringValues: [userId], + }); + + return userRoles; + }, + + getDefaultRole() { + const defaultRole = rowService.get({ + tableName: ROLES_TABLE, + whereString: `WHERE ${tableFields.ROLE_NAME}=?`, + whereStringValues: [constantRoles.DEFAULT_ROLE], + }); + + return defaultRole; + }, + }; +}; diff --git a/src/services/index.js b/src/services/index.js index 93e6d13..f87674a 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -2,5 +2,6 @@ const db = require('../db'); const rowService = require('./rowService')(db); const tableService = require('./tableService')(db); +const authService = require('./authService')(db); -module.exports = { rowService, tableService }; +module.exports = { rowService, tableService, authService }; diff --git a/src/swagger/swagger.json b/src/swagger/swagger.json index 1df994d..2dad615 100644 --- a/src/swagger/swagger.json +++ b/src/swagger/swagger.json @@ -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" } } }