diff --git a/server/src/api/controllers/InviteUsers.ts b/server/src/api/controllers/InviteUsers.ts index 831f995a6..1a78e41c1 100644 --- a/server/src/api/controllers/InviteUsers.ts +++ b/server/src/api/controllers/InviteUsers.ts @@ -1,6 +1,7 @@ import { Service, Inject } from 'typedi'; import { Router, Request, Response } from 'express'; import { check, body, param } from 'express-validator'; +import { IInviteUserInput } from 'interfaces'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import InviteUserService from 'services/InviteUsers'; import { ServiceErrors, ServiceError } from 'exceptions'; @@ -22,7 +23,7 @@ export default class InviteUsersController extends BaseController { [body('email').exists().trim().escape()], this.validationResult, asyncMiddleware(this.sendInvite.bind(this)), - this.handleServicesError, + this.handleServicesError ); return router; } @@ -38,14 +39,14 @@ export default class InviteUsersController extends BaseController { [...this.inviteUserDTO], this.validationResult, asyncMiddleware(this.accept.bind(this)), - this.handleServicesError, + this.handleServicesError ); router.get( '/invited/:token', [param('token').exists().trim().escape()], this.validationResult, asyncMiddleware(this.invited.bind(this)), - this.handleServicesError, + this.handleServicesError ); return router; @@ -76,8 +77,11 @@ export default class InviteUsersController extends BaseController { const { user } = req; try { - await this.inviteUsersService.sendInvite(tenantId, email, user); - + const { invite } = await this.inviteUsersService.sendInvite( + tenantId, + email, + user + ); return res.status(200).send({ type: 'success', code: 'INVITE.SENT.SUCCESSFULLY', @@ -104,6 +108,7 @@ export default class InviteUsersController extends BaseController { try { await this.inviteUsersService.acceptInvite(token, inviteUserInput); + return res.status(200).send({ type: 'success', code: 'USER.INVITE.ACCEPTED', @@ -144,19 +149,40 @@ export default class InviteUsersController extends BaseController { */ handleServicesError(error, req: Request, res: Response, next: Function) { if (error instanceof ServiceError) { + if (error.errorType === 'EMAIL_EXISTS') { + return res.status(400).send({ + errors: [{ + type: 'EMAIL.ALREADY.EXISTS', + code: 100, + message: 'Email already exists in the users.' + }], + }); + } if (error.errorType === 'EMAIL_ALREADY_INVITED') { return res.status(400).send({ - errors: [{ type: 'EMAIL.ALREADY.INVITED' }], + errors: [{ + type: 'EMAIL.ALREADY.INVITED', + code: 200, + message: 'Email already invited.', + }], }); } if (error.errorType === 'INVITE_TOKEN_INVALID') { return res.status(400).send({ - errors: [{ type: 'INVITE.TOKEN.INVALID' }], + errors: [{ + type: 'INVITE.TOKEN.INVALID', + code: 300, + message: 'Invite token is invalid, please try another one.', + }], }); } if (error.errorType === 'PHONE_NUMBER_EXISTS') { return res.status(400).send({ - errors: [{ type: 'PHONE_NUMBER.EXISTS' }], + errors: [{ + type: 'PHONE_NUMBER.EXISTS', + code: 400, + message: 'Phone number is already invited, please try another unique one.' + }], }); } } diff --git a/server/src/api/controllers/Ping.ts b/server/src/api/controllers/Ping.ts index 0dd89f3f0..df199d8c2 100644 --- a/server/src/api/controllers/Ping.ts +++ b/server/src/api/controllers/Ping.ts @@ -2,7 +2,7 @@ import { Router, Request, Response } from 'express'; export default class Ping { /** - * Router constur + * Router constructor. */ router() { const router = Router(); diff --git a/server/src/api/controllers/Users.ts b/server/src/api/controllers/Users.ts index ba83d2465..ef35a71f7 100644 --- a/server/src/api/controllers/Users.ts +++ b/server/src/api/controllers/Users.ts @@ -126,6 +126,7 @@ export default class UsersController extends BaseController{ try { await this.usersService.deleteUser(tenantId, id); + return res.status(200).send({ id, message: 'The user has been deleted successfully.' @@ -225,10 +226,10 @@ export default class UsersController extends BaseController{ if (error instanceof ServiceErrors) { const errorReasons = []; - if (error.errorType === 'email_already_exists') { + if (error.errorType === 'EMAIL_ALREADY_EXISTS') { errorReasons.push({ type: 'EMAIL_ALREADY_EXIST', code: 100 }); } - if (error.errorType === 'phone_number_already_exist') { + if (error.errorType === 'PHONE_NUMBER_ALREADY_EXIST') { errorReasons.push({ type: 'PHONE_NUMBER_ALREADY_EXIST', code: 200 }); } if (errorReasons.length > 0) { @@ -236,30 +237,36 @@ export default class UsersController extends BaseController{ } } if (error instanceof ServiceError) { - if (error.errorType === 'user_not_found') { + if (error.errorType === 'USER_NOT_FOUND') { return res.boom.badRequest( 'User not found.', { errors: [{ type: 'USER.NOT.FOUND', code: 100 }] } ); } - if (error.errorType === 'user_already_active') { + if (error.errorType === 'USER_ALREADY_ACTIVE') { return res.boom.badRequest( 'User is already active.', { errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }] }, ); } - if (error.errorType === 'user_already_inactive') { + if (error.errorType === 'USER_ALREADY_INACTIVE') { return res.boom.badRequest( 'User is already inactive.', { errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }] }, ); } - if (error.errorType === 'user_same_the_authorized_user') { + if (error.errorType === 'USER_SAME_THE_AUTHORIZED_USER') { return res.boom.badRequest( 'You could not activate/inactivate the same authorized user.', { errors: [{ type: 'CANNOT.TOGGLE.ACTIVATE.AUTHORIZED.USER', code: 300 }] }, ) } + if (error.errorType === 'CANNOT_DELETE_LAST_USER') { + return res.boom.badRequest( + 'Cannot delete last user in the organization.', + { errors: [{ type: 'CANNOT_DELETE_LAST_USER', code: 400 }] }, + ); + } } next(error); } diff --git a/server/src/api/middleware/AttachCurrentTenantUser.ts b/server/src/api/middleware/AttachCurrentTenantUser.ts index 0fc1d9034..0ba44f689 100644 --- a/server/src/api/middleware/AttachCurrentTenantUser.ts +++ b/server/src/api/middleware/AttachCurrentTenantUser.ts @@ -14,7 +14,6 @@ const attachCurrentUser = async (req: Request, res: Response, next: Function) => try { Logger.info('[attach_user_middleware] finding system user by id.'); const user = await systemUserRepository.findOneById(req.token.id); - console.log(user); if (!user) { Logger.info('[attach_user_middleware] the system user not found.'); diff --git a/server/src/api/middleware/EnsureTenantIsSeeded.ts b/server/src/api/middleware/EnsureTenantIsSeeded.ts index 20d31e690..69f92c76a 100644 --- a/server/src/api/middleware/EnsureTenantIsSeeded.ts +++ b/server/src/api/middleware/EnsureTenantIsSeeded.ts @@ -9,11 +9,13 @@ export default (req: Request, res: Response, next: Function) => { throw new Error('Should load this middleware after `TenancyMiddleware`.'); } if (!req.tenant.seededAt) { - Logger.info('[ensure_tenant_initialized_middleware] tenant databae not seeded.'); + Logger.info( + '[ensure_tenant_initialized_middleware] tenant databae not seeded.' + ); return res.boom.badRequest( 'Tenant database is not seeded with initial data yet.', - { errors: [{ type: 'TENANT.DATABASE.NOT.SEED' }] }, + { errors: [{ type: 'TENANT.DATABASE.NOT.SEED' }] } ); } next(); -}; \ No newline at end of file +}; diff --git a/server/src/api/middleware/SubscriptionMiddleware.ts b/server/src/api/middleware/SubscriptionMiddleware.ts index 17e2ee5c8..ce7d45258 100644 --- a/server/src/api/middleware/SubscriptionMiddleware.ts +++ b/server/src/api/middleware/SubscriptionMiddleware.ts @@ -1,7 +1,11 @@ import { Request, Response, NextFunction } from 'express'; import { Container } from 'typedi'; -export default (subscriptionSlug = 'main') => async (req: Request, res: Response, next: NextFunction) => { +export default (subscriptionSlug = 'main') => async ( + req: Request, + res: Response, + next: NextFunction +) => { const { tenant, tenantId } = req; const Logger = Container.get('logger'); const { subscriptionRepository } = Container.get('repositories'); @@ -10,22 +14,28 @@ export default (subscriptionSlug = 'main') => async (req: Request, res: Response throw new Error('Should load `TenancyMiddlware` before this middleware.'); } Logger.info('[subscription_middleware] trying get tenant main subscription.'); - const subscription = await subscriptionRepository.getBySlugInTenant(subscriptionSlug, tenantId); - + const subscription = await subscriptionRepository.getBySlugInTenant( + subscriptionSlug, + tenantId + ); // Validate in case there is no any already subscription. if (!subscription) { - Logger.info('[subscription_middleware] tenant has no subscription.', { tenantId }); - return res.boom.badRequest( - 'Tenant has no subscription.', - { errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }] } - ); + Logger.info('[subscription_middleware] tenant has no subscription.', { + tenantId, + }); + return res.boom.badRequest('Tenant has no subscription.', { + errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }], + }); } // Validate in case the subscription is inactive. else if (subscription.inactive()) { - Logger.info('[subscription_middleware] tenant main subscription is expired.', { tenantId }); + Logger.info( + '[subscription_middleware] tenant main subscription is expired.', + { tenantId } + ); return res.boom.badRequest(null, { errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }], }); } next(); -}; \ No newline at end of file +}; diff --git a/server/src/api/middleware/asyncMiddleware.ts b/server/src/api/middleware/asyncMiddleware.ts index a8c0f747b..ece9dd2cf 100644 --- a/server/src/api/middleware/asyncMiddleware.ts +++ b/server/src/api/middleware/asyncMiddleware.ts @@ -1,5 +1,3 @@ -import logger from "src/loaders/logger"; - import { Request, Response, NextFunction } from 'express'; import { Container } from 'typedi'; diff --git a/server/src/collection/SoftDeleteQueryBuilder.js b/server/src/collection/SoftDeleteQueryBuilder.js new file mode 100644 index 000000000..2bd15fe30 --- /dev/null +++ b/server/src/collection/SoftDeleteQueryBuilder.js @@ -0,0 +1,73 @@ +import moment from 'moment'; +import { Model } from 'objection'; + +const options = { + columnName: 'deleted_at', + deletedValue: moment().format('YYYY-MM-DD HH:mm:ss'), + notDeletedValue: null, +}; + +export default class SoftDeleteQueryBuilder extends Model.QueryBuilder { + constructor(...args) { + super(...args); + + this.onBuild((builder) => { + if (builder.isFind() || builder.isDelete() || builder.isUpdate()) { + builder.whereNotDeleted(); + } + }); + } + + /** + * override the normal delete function with one that patches the row's "deleted" column + */ + delete() { + this.context({ + softDelete: true, + }); + const patch = {}; + patch[options.columnName] = options.deletedValue; + return this.patch(patch); + } + + /** + * Provide a way to actually delete the row if necessary + */ + hardDelete() { + return super.delete(); + } + + /** + * Provide a way to undo the delete + */ + undelete() { + this.context({ + undelete: true, + }); + const patch = {}; + patch[options.columnName] = options.notDeletedValue; + return this.patch(patch); + } + + /** + * Provide a way to filter to ONLY deleted records without having to remember the column name + */ + whereDeleted() { + const prefix = this.modelClass().tableName; + + // this if is for backwards compatibility, to protect those that used a nullable `deleted` field + if (options.deletedValue === true) { + return this.where(`${prefix}.${options.columnName}`, options.deletedValue); + } + // qualify the column name + return this.whereNot(`${prefix}.${options.columnName}`, options.notDeletedValue); + } + + // provide a way to filter out deleted records without having to remember the column name + whereNotDeleted() { + const prefix = this.modelClass().tableName; + + // qualify the column name + return this.where(`${prefix}.${options.columnName}`, options.notDeletedValue); + } +} diff --git a/server/src/interfaces/User.ts b/server/src/interfaces/User.ts index 4389c0613..162d1b879 100644 --- a/server/src/interfaces/User.ts +++ b/server/src/interfaces/User.ts @@ -1,6 +1,6 @@ +import { Model } from 'objection'; - -export interface ISystemUser { +export interface ISystemUser extends Model { id: number, firstName: string, lastName: string, @@ -34,4 +34,12 @@ export interface IInviteUserInput { lastName: string, phoneNumber: string, password: string, +}; + +export interface IUserInvite { + id: number, + email: string, + token: string, + tenantId: number, + createdAt?: Date, } \ No newline at end of file diff --git a/server/src/repositories/EntityRepository.ts b/server/src/repositories/EntityRepository.ts index e948d6068..75705998c 100644 --- a/server/src/repositories/EntityRepository.ts +++ b/server/src/repositories/EntityRepository.ts @@ -1,4 +1,4 @@ -import { cloneDeep, cloneDeepWith, forOwn, isString } from 'lodash'; +import { cloneDeep, forOwn, isString } from 'lodash'; import ModelEntityNotFound from 'exceptions/ModelEntityNotFound'; export default class EntityRepository { @@ -38,8 +38,7 @@ export default class EntityRepository { * @returns {Promise} - query builder. You can chain additional methods to it or call "await" or then() on it to execute */ find(attributeValues = {}, withRelations?) { - return this.model - .query() + return this.model.query() .where(attributeValues) .withGraphFetched(withRelations); } diff --git a/server/src/services/Authentication/index.ts b/server/src/services/Authentication/index.ts index 18e3564ed..f7d6856bc 100644 --- a/server/src/services/Authentication/index.ts +++ b/server/src/services/Authentication/index.ts @@ -23,7 +23,6 @@ import AuthenticationMailMessages from 'services/Authentication/AuthenticationMa import AuthenticationSMSMessages from 'services/Authentication/AuthenticationSMSMessages'; import TenantsManager from 'services/Tenancy/TenantsManager'; - const ERRORS = { INVALID_DETAILS: 'INVALID_DETAILS', USER_INACTIVE: 'USER_INACTIVE', @@ -32,7 +31,7 @@ const ERRORS = { USER_NOT_FOUND: 'USER_NOT_FOUND', TOKEN_EXPIRED: 'TOKEN_EXPIRED', PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS', - EMAIL_EXISTS: 'EMAIL_EXISTS' + EMAIL_EXISTS: 'EMAIL_EXISTS', }; @Service() export default class AuthenticationService implements IAuthenticationService { @@ -136,6 +135,7 @@ export default class AuthenticationService implements IAuthenticationService { */ private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) { const { systemUserRepository } = this.sysRepositories; + const isEmailExists = await systemUserRepository.findOneByEmail( registerDTO.email ); @@ -279,7 +279,10 @@ export default class AuthenticationService implements IAuthenticationService { const hashedPassword = await hashPassword(password); this.logger.info('[reset_password] saving a new hashed password.'); - await systemUserRepository.update({ password: hashedPassword }, { id: user.id }); + await systemUserRepository.update( + { password: hashedPassword }, + { id: user.id } + ); // Deletes the used token. await this.deletePasswordResetToken(tokenModel.email); diff --git a/server/src/services/InviteUsers/index.ts b/server/src/services/InviteUsers/index.ts index b44ae3204..d903f6334 100644 --- a/server/src/services/InviteUsers/index.ts +++ b/server/src/services/InviteUsers/index.ts @@ -12,13 +12,14 @@ import { hashPassword } from 'utils'; import TenancyService from 'services/Tenancy/TenancyService'; import InviteUsersMailMessages from 'services/InviteUsers/InviteUsersMailMessages'; import events from 'subscribers/events'; -import { ISystemUser, IInviteUserInput } from 'interfaces'; +import { ISystemUser, IInviteUserInput, IUserInvite } from 'interfaces'; import TenantsManagerService from 'services/Tenancy/TenantsManager'; const ERRORS = { EMAIL_ALREADY_INVITED: 'EMAIL_ALREADY_INVITED', INVITE_TOKEN_INVALID: 'INVITE_TOKEN_INVALID', - PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS' + PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS', + EMAIL_EXISTS: 'EMAIL_EXISTS' }; @Service() export default class InviteUserService { @@ -66,12 +67,16 @@ export default class InviteUserService { const user = await systemUserRepository.findOneByEmail(inviteToken.email); // Sets the invited user details after invite accepting. - const updateUserOper = systemUserRepository.update({ - ...inviteUserInput, - active: 1, - inviteAcceptedAt: moment().format('YYYY-MM-DD'), - password: hashedPassword, - }, { id: user.id }); + const systemUserOper = systemUserRepository.create( + { + ...inviteUserInput, + email: inviteToken.email, + tenantId: inviteToken.tenantId, + active: 1, + inviteAcceptedAt: moment().format('YYYY-MM-DD'), + password: hashedPassword, + }, + ); this.logger.info('[accept_invite] trying to delete the given token.'); const deleteInviteTokenOper = Invite.query() @@ -79,14 +84,14 @@ export default class InviteUserService { .delete(); // Await all async operations. - const [updatedUser] = await Promise.all([ - updateUserOper, + const [systemUser] = await Promise.all([ + systemUserOper, deleteInviteTokenOper, ]); // Triggers `onUserAcceptInvite` event. this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, { inviteToken, - user: updatedUser, + user: systemUser, }); } @@ -96,21 +101,21 @@ export default class InviteUserService { * @param {string} email - * @param {IUser} authorizedUser - * - * @return {Promise} + * @return {Promise} */ public async sendInvite( tenantId: number, email: string, authorizedUser: ISystemUser ): Promise<{ - invite: IInvite, - user: ISystemUser + invite: IUserInvite; }> { - const { systemUserRepository } = this.sysRepositories; - // Throw error in case user email exists. await this.throwErrorIfUserEmailExists(email); + // Throws service error in case the user already invited. + await this.throwErrorIfUserInvited(email); + this.logger.info('[send_invite] trying to store invite token.'); const invite = await Invite.query().insert({ email, @@ -121,16 +126,13 @@ export default class InviteUserService { this.logger.info( '[send_invite] trying to store user with email and tenant.' ); - const user = await systemUserRepository.create({ - email, - tenant_id: authorizedUser.tenantId, - active: 1, - }); // Triggers `onUserSendInvite` event. this.eventDispatcher.dispatch(events.inviteUser.sendInvite, { - invite, authorizedUser, tenantId + invite, + authorizedUser, + tenantId, }); - return { invite, user }; + return { invite }; } /** @@ -140,7 +142,7 @@ export default class InviteUserService { */ public async checkInvite( token: string - ): Promise<{ inviteToken: string; orgName: object }> { + ): Promise<{ inviteToken: IUserInvite; orgName: object }> { const inviteToken = await this.getInviteOrThrowError(token); // Find the tenant that associated to the given token. @@ -170,14 +172,27 @@ export default class InviteUserService { */ private async throwErrorIfUserEmailExists( email: string - ): Promise { + ): Promise { const { systemUserRepository } = this.sysRepositories; const foundUser = await systemUserRepository.findOneByEmail(email); if (foundUser) { + throw new ServiceError(ERRORS.EMAIL_EXISTS); + } + } + + /** + * Throws service error if the user already invited. + * @param {string} email - + */ + private async throwErrorIfUserInvited( + email: string, + ): Promise { + const inviteToken = await Invite.query().findOne('email', email); + + if (inviteToken) { throw new ServiceError(ERRORS.EMAIL_ALREADY_INVITED); } - return foundUser; } /** @@ -186,7 +201,7 @@ export default class InviteUserService { * @throws {ServiceError} * @returns {Invite} */ - private async getInviteOrThrowError(token: string) { + private async getInviteOrThrowError(token: string): Promise { const inviteToken = await Invite.query().findOne('token', token); if (!inviteToken) { diff --git a/server/src/services/Users/UsersService.ts b/server/src/services/Users/UsersService.ts index d6255ec8e..22e623689 100644 --- a/server/src/services/Users/UsersService.ts +++ b/server/src/services/Users/UsersService.ts @@ -1,9 +1,17 @@ -import { Inject, Service } from 'typedi'; import TenancyService from 'services/Tenancy/TenancyService'; -import { SystemUser } from 'system/models'; +import { Inject, Service } from 'typedi'; import { ServiceError, ServiceErrors } from 'exceptions'; import { ISystemUser, ISystemUserDTO } from 'interfaces'; -import systemRepositories from 'loaders/systemRepositories'; + +const ERRORS = { + CANNOT_DELETE_LAST_USER: 'CANNOT_DELETE_LAST_USER', + USER_ALREADY_ACTIVE: 'USER_ALREADY_ACTIVE', + USER_ALREADY_INACTIVE: 'USER_ALREADY_INACTIVE', + EMAIL_ALREADY_EXISTS: 'EMAIL_ALREADY_EXISTS', + PHONE_NUMBER_ALREADY_EXIST: 'PHONE_NUMBER_ALREADY_EXIST', + USER_NOT_FOUND: 'USER_NOT_FOUND', + USER_SAME_THE_AUTHORIZED_USER: 'USER_SAME_THE_AUTHORIZED_USER', +}; @Service() export default class UsersService { @@ -23,7 +31,7 @@ export default class UsersService { * @param {IUserDTO} userDTO * @return {Promise} */ - async editUser( + public async editUser( tenantId: number, userId: number, userDTO: ISystemUserDTO @@ -36,49 +44,24 @@ export default class UsersService { }); const userByPhoneNumber = await systemUserRepository.findOne({ phoneNumber: userDTO.phoneNumber, - id: userId + id: userId, }); const serviceErrors: ServiceError[] = []; if (userByEmail) { - serviceErrors.push(new ServiceError('email_already_exists')); + serviceErrors.push(new ServiceError(ERRORS.EMAIL_ALREADY_EXISTS)); } if (userByPhoneNumber) { - serviceErrors.push(new ServiceError('phone_number_already_exist')); + serviceErrors.push(new ServiceError(ERRORS.PHONE_NUMBER_ALREADY_EXIST)); } if (serviceErrors.length > 0) { throw new ServiceErrors(serviceErrors); } - const updateSystemUser = await systemUserRepository - .update({ ...userDTO, }, { id: userId }); - - return updateSystemUser; - } - - /** - * Validate user existance throw error in case user was not found., - * @param {number} tenantId - - * @param {number} userId - - * @returns {ISystemUser} - */ - async getUserOrThrowError( - tenantId: number, - userId: number - ): Promise { - const { systemUserRepository } = this.repositories; - const user = await systemUserRepository.findOneByIdAndTenant( - userId, - tenantId + const updateSystemUser = await systemUserRepository.update( + { ...userDTO }, + { id: userId } ); - - if (!user) { - this.logger.info('[users] the given user not found.', { - tenantId, - userId, - }); - throw new ServiceError('user_not_found'); - } - return user; + return updateSystemUser; } /** @@ -86,14 +69,20 @@ export default class UsersService { * @param {number} tenantId * @param {number} userId */ - async deleteUser(tenantId: number, userId: number): Promise { + public async deleteUser(tenantId: number, userId: number): Promise { const { systemUserRepository } = this.repositories; - await this.getUserOrThrowError(tenantId, userId); + + // Retrieve user details or throw not found service error. + const oldUser = await this.getUserOrThrowError(tenantId, userId); this.logger.info('[users] trying to delete the given user.', { tenantId, userId, }); + // Validate the delete user should not be the last user. + await this.validateNotLastUserDelete(tenantId); + + // Delete user from the storage. await systemUserRepository.deleteById(userId); this.logger.info('[users] the given user deleted successfully.', { @@ -104,18 +93,24 @@ export default class UsersService { /** * Activate the given user id. - * @param {number} tenantId - * @param {number} userId + * @param {number} tenantId - Tenant id. + * @param {number} userId - User id. + * @return {Promise} */ - async activateUser( + public async activateUser( tenantId: number, userId: number, authorizedUser: ISystemUser ): Promise { - this.throwErrorIfUserIdSameAuthorizedUser(userId, authorizedUser); const { systemUserRepository } = this.repositories; + // Throw service error if the given user is equals the authorized user. + this.throwErrorIfUserSameAuthorizedUser(userId, authorizedUser); + + // Retrieve the user or throw not found service error. const user = await this.getUserOrThrowError(tenantId, userId); + + // Throw serivce error if the user is already activated. this.throwErrorIfUserActive(user); await systemUserRepository.activateUser(userId); @@ -127,15 +122,20 @@ export default class UsersService { * @param {number} userId * @return {Promise} */ - async inactivateUser( + public async inactivateUser( tenantId: number, userId: number, authorizedUser: ISystemUser ): Promise { - this.throwErrorIfUserIdSameAuthorizedUser(userId, authorizedUser); const { systemUserRepository } = this.repositories; + // Throw service error if the given user is equals the authorized user. + this.throwErrorIfUserSameAuthorizedUser(userId, authorizedUser); + + // Retrieve the user or throw not found service error. const user = await this.getUserOrThrowError(tenantId, userId); + + // Throw serivce error if the user is already inactivated. this.throwErrorIfUserInactive(user); await systemUserRepository.inactivateById(userId); @@ -146,10 +146,10 @@ export default class UsersService { * @param {number} tenantId * @param {object} filter */ - async getList(tenantId: number) { - const users = await SystemUser.query() - .whereNotDeleted() - .where('tenant_id', tenantId); + public async getList(tenantId: number) { + const { systemUserRepository } = this.repositories; + + const users = await systemUserRepository.find({ tenantId }); return users; } @@ -159,18 +159,58 @@ export default class UsersService { * @param {number} tenantId - Tenant id. * @param {number} userId - User id. */ - async getUser(tenantId: number, userId: number) { + public async getUser(tenantId: number, userId: number) { return this.getUserOrThrowError(tenantId, userId); } + /** + * Validate user existance throw error in case user was not found., + * @param {number} tenantId - + * @param {number} userId - + * @returns {ISystemUser} + */ + async getUserOrThrowError( + tenantId: number, + userId: number + ): Promise { + const { systemUserRepository } = this.repositories; + + const user = await systemUserRepository.findOneByIdAndTenant( + userId, + tenantId + ); + if (!user) { + this.logger.info('[users] the given user not found.', { + tenantId, + userId, + }); + throw new ServiceError(ERRORS.USER_NOT_FOUND); + } + return user; + } + + /** + * Validate the delete user should not be the last user. + * @param {number} tenantId + */ + private async validateNotLastUserDelete(tenantId: number) { + const { systemUserRepository } = this.repositories; + + const usersFound = await systemUserRepository.find({ tenantId }); + + if (usersFound.length === 1) { + throw new ServiceError(ERRORS.CANNOT_DELETE_LAST_USER); + } + } + /** * Throws service error in case the user was already active. * @param {ISystemUser} user * @throws {ServiceError} */ - throwErrorIfUserActive(user: ISystemUser) { + private throwErrorIfUserActive(user: ISystemUser) { if (user.active) { - throw new ServiceError('user_already_active'); + throw new ServiceError(ERRORS.USER_ALREADY_ACTIVE); } } @@ -179,9 +219,9 @@ export default class UsersService { * @param {ISystemUser} user * @throws {ServiceError} */ - throwErrorIfUserInactive(user: ISystemUser) { + private throwErrorIfUserInactive(user: ISystemUser) { if (!user.active) { - throw new ServiceError('user_already_inactive'); + throw new ServiceError(ERRORS.USER_ALREADY_INACTIVE); } } @@ -190,12 +230,12 @@ export default class UsersService { * @param {number} userId * @param {ISystemUser} authorizedUser */ - throwErrorIfUserIdSameAuthorizedUser( + private throwErrorIfUserSameAuthorizedUser( userId: number, authorizedUser: ISystemUser ) { if (userId === authorizedUser.id) { - throw new ServiceError('user_same_the_authorized_user'); + throw new ServiceError(ERRORS.USER_SAME_THE_AUTHORIZED_USER); } } } diff --git a/server/src/subscribers/SaleInvoices/SyncCustomersBalance.ts b/server/src/subscribers/SaleInvoices/SyncCustomersBalance.ts index 2e1094348..015a85ead 100644 --- a/server/src/subscribers/SaleInvoices/SyncCustomersBalance.ts +++ b/server/src/subscribers/SaleInvoices/SyncCustomersBalance.ts @@ -59,7 +59,6 @@ export default class SaleInvoiceSubscriber { ); } - /** * Handles customer balance decrement once sale invoice deleted. */ diff --git a/server/src/system/migrations/20200420134633_create_users_table.js b/server/src/system/migrations/20200420134633_create_users_table.js index d4a08a226..de5071ad7 100644 --- a/server/src/system/migrations/20200420134633_create_users_table.js +++ b/server/src/system/migrations/20200420134633_create_users_table.js @@ -4,8 +4,8 @@ exports.up = function (knex) { table.increments(); table.string('first_name'); table.string('last_name'); - table.string('email').unique().index(); - table.string('phone_number').unique().index(); + table.string('email').index(); + table.string('phone_number').index(); table.string('password'); table.boolean('active').index(); table.string('language'); diff --git a/server/src/system/models/SystemUser.js b/server/src/system/models/SystemUser.js index 5a360626b..a1f5bed74 100644 --- a/server/src/system/models/SystemUser.js +++ b/server/src/system/models/SystemUser.js @@ -1,14 +1,9 @@ -import { Model, mixin } from 'objection'; +import { Model } from 'objection'; import bcrypt from 'bcryptjs'; -import SoftDelete from 'objection-soft-delete'; import SystemModel from 'system/models/SystemModel'; -import moment from 'moment'; +import SoftDeleteQueryBuilder from 'collection/SoftDeleteQueryBuilder'; -export default class SystemUser extends mixin(SystemModel, [SoftDelete({ - columnName: 'deleted_at', - deletedValue: moment().format('YYYY-MM-DD HH:mm:ss'), - notDeletedValue: null, -})]) { +export default class SystemUser extends SystemModel { /** * Table name. */ @@ -16,6 +11,13 @@ export default class SystemUser extends mixin(SystemModel, [SoftDelete({ return 'users'; } + /** + * Soft delete query builder. + */ + static get QueryBuilder() { + return SoftDeleteQueryBuilder; + } + /** * Timestamps columns. */ @@ -23,10 +25,16 @@ export default class SystemUser extends mixin(SystemModel, [SoftDelete({ return ['createdAt', 'updatedAt']; } + /** + * Virtual attributes. + */ static get virtualAttributes() { return ['fullName']; } + /** + * Full name attribute. + */ get fullName() { return (this.firstName + ' ' + this.lastName).trim(); } diff --git a/server/src/system/repositories/SystemUserRepository.ts b/server/src/system/repositories/SystemUserRepository.ts index fcd416d2d..6e8f7d4eb 100644 --- a/server/src/system/repositories/SystemUserRepository.ts +++ b/server/src/system/repositories/SystemUserRepository.ts @@ -22,7 +22,6 @@ export default class SystemUserRepository extends SystemRepository { return this.cache.get(cacheKey, () => { return this.model.query() - .whereNotDeleted() .findOne('email', crediential) .orWhere('phone_number', crediential); }); @@ -39,7 +38,6 @@ export default class SystemUserRepository extends SystemRepository { return this.cache.get(cacheKey, () => { return this.model.query() - .whereNotDeleted() .findOne({ id: userId, tenant_id: tenantId }); }); } @@ -53,7 +51,7 @@ export default class SystemUserRepository extends SystemRepository { const cacheKey = this.getCacheKey('findOneByEmail', email); return this.cache.get(cacheKey, () => { - return this.model.query().whereNotDeleted().findOne('email', email); + return this.model.query().findOne('email', email); }); } @@ -67,7 +65,6 @@ export default class SystemUserRepository extends SystemRepository { return this.cache.get(cacheKey, () => { return this.model.query() - .whereNotDeleted() .findOne('phoneNumber', phoneNumber); }); }