fix: invite user to the system.

fix: soft delete system user.
This commit is contained in:
a.bouhuolia
2021-01-19 13:18:48 +02:00
parent ef53b25c18
commit 59f44d9ef6
17 changed files with 320 additions and 136 deletions

View File

@@ -1,6 +1,7 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Router, Request, Response } from 'express'; import { Router, Request, Response } from 'express';
import { check, body, param } from 'express-validator'; import { check, body, param } from 'express-validator';
import { IInviteUserInput } from 'interfaces';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import InviteUserService from 'services/InviteUsers'; import InviteUserService from 'services/InviteUsers';
import { ServiceErrors, ServiceError } from 'exceptions'; import { ServiceErrors, ServiceError } from 'exceptions';
@@ -22,7 +23,7 @@ export default class InviteUsersController extends BaseController {
[body('email').exists().trim().escape()], [body('email').exists().trim().escape()],
this.validationResult, this.validationResult,
asyncMiddleware(this.sendInvite.bind(this)), asyncMiddleware(this.sendInvite.bind(this)),
this.handleServicesError, this.handleServicesError
); );
return router; return router;
} }
@@ -38,14 +39,14 @@ export default class InviteUsersController extends BaseController {
[...this.inviteUserDTO], [...this.inviteUserDTO],
this.validationResult, this.validationResult,
asyncMiddleware(this.accept.bind(this)), asyncMiddleware(this.accept.bind(this)),
this.handleServicesError, this.handleServicesError
); );
router.get( router.get(
'/invited/:token', '/invited/:token',
[param('token').exists().trim().escape()], [param('token').exists().trim().escape()],
this.validationResult, this.validationResult,
asyncMiddleware(this.invited.bind(this)), asyncMiddleware(this.invited.bind(this)),
this.handleServicesError, this.handleServicesError
); );
return router; return router;
@@ -76,8 +77,11 @@ export default class InviteUsersController extends BaseController {
const { user } = req; const { user } = req;
try { try {
await this.inviteUsersService.sendInvite(tenantId, email, user); const { invite } = await this.inviteUsersService.sendInvite(
tenantId,
email,
user
);
return res.status(200).send({ return res.status(200).send({
type: 'success', type: 'success',
code: 'INVITE.SENT.SUCCESSFULLY', code: 'INVITE.SENT.SUCCESSFULLY',
@@ -104,6 +108,7 @@ export default class InviteUsersController extends BaseController {
try { try {
await this.inviteUsersService.acceptInvite(token, inviteUserInput); await this.inviteUsersService.acceptInvite(token, inviteUserInput);
return res.status(200).send({ return res.status(200).send({
type: 'success', type: 'success',
code: 'USER.INVITE.ACCEPTED', code: 'USER.INVITE.ACCEPTED',
@@ -144,19 +149,40 @@ export default class InviteUsersController extends BaseController {
*/ */
handleServicesError(error, req: Request, res: Response, next: Function) { handleServicesError(error, req: Request, res: Response, next: Function) {
if (error instanceof ServiceError) { 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') { if (error.errorType === 'EMAIL_ALREADY_INVITED') {
return res.status(400).send({ 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') { if (error.errorType === 'INVITE_TOKEN_INVALID') {
return res.status(400).send({ 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') { if (error.errorType === 'PHONE_NUMBER_EXISTS') {
return res.status(400).send({ 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.'
}],
}); });
} }
} }

View File

@@ -2,7 +2,7 @@ import { Router, Request, Response } from 'express';
export default class Ping { export default class Ping {
/** /**
* Router constur * Router constructor.
*/ */
router() { router() {
const router = Router(); const router = Router();

View File

@@ -126,6 +126,7 @@ export default class UsersController extends BaseController{
try { try {
await this.usersService.deleteUser(tenantId, id); await this.usersService.deleteUser(tenantId, id);
return res.status(200).send({ return res.status(200).send({
id, id,
message: 'The user has been deleted successfully.' message: 'The user has been deleted successfully.'
@@ -225,10 +226,10 @@ export default class UsersController extends BaseController{
if (error instanceof ServiceErrors) { if (error instanceof ServiceErrors) {
const errorReasons = []; const errorReasons = [];
if (error.errorType === 'email_already_exists') { if (error.errorType === 'EMAIL_ALREADY_EXISTS') {
errorReasons.push({ type: 'EMAIL_ALREADY_EXIST', code: 100 }); 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 }); errorReasons.push({ type: 'PHONE_NUMBER_ALREADY_EXIST', code: 200 });
} }
if (errorReasons.length > 0) { if (errorReasons.length > 0) {
@@ -236,30 +237,36 @@ export default class UsersController extends BaseController{
} }
} }
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'user_not_found') { if (error.errorType === 'USER_NOT_FOUND') {
return res.boom.badRequest( return res.boom.badRequest(
'User not found.', 'User not found.',
{ errors: [{ type: 'USER.NOT.FOUND', code: 100 }] } { errors: [{ type: 'USER.NOT.FOUND', code: 100 }] }
); );
} }
if (error.errorType === 'user_already_active') { if (error.errorType === 'USER_ALREADY_ACTIVE') {
return res.boom.badRequest( return res.boom.badRequest(
'User is already active.', 'User is already active.',
{ errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }] }, { errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }] },
); );
} }
if (error.errorType === 'user_already_inactive') { if (error.errorType === 'USER_ALREADY_INACTIVE') {
return res.boom.badRequest( return res.boom.badRequest(
'User is already inactive.', 'User is already inactive.',
{ errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }] }, { 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( return res.boom.badRequest(
'You could not activate/inactivate the same authorized user.', 'You could not activate/inactivate the same authorized user.',
{ errors: [{ type: 'CANNOT.TOGGLE.ACTIVATE.AUTHORIZED.USER', code: 300 }] }, { 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); next(error);
} }

View File

@@ -14,7 +14,6 @@ const attachCurrentUser = async (req: Request, res: Response, next: Function) =>
try { try {
Logger.info('[attach_user_middleware] finding system user by id.'); Logger.info('[attach_user_middleware] finding system user by id.');
const user = await systemUserRepository.findOneById(req.token.id); const user = await systemUserRepository.findOneById(req.token.id);
console.log(user);
if (!user) { if (!user) {
Logger.info('[attach_user_middleware] the system user not found.'); Logger.info('[attach_user_middleware] the system user not found.');

View File

@@ -9,11 +9,13 @@ export default (req: Request, res: Response, next: Function) => {
throw new Error('Should load this middleware after `TenancyMiddleware`.'); throw new Error('Should load this middleware after `TenancyMiddleware`.');
} }
if (!req.tenant.seededAt) { 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( return res.boom.badRequest(
'Tenant database is not seeded with initial data yet.', 'Tenant database is not seeded with initial data yet.',
{ errors: [{ type: 'TENANT.DATABASE.NOT.SEED' }] }, { errors: [{ type: 'TENANT.DATABASE.NOT.SEED' }] }
); );
} }
next(); next();
}; };

View File

@@ -1,7 +1,11 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; 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 { tenant, tenantId } = req;
const Logger = Container.get('logger'); const Logger = Container.get('logger');
const { subscriptionRepository } = Container.get('repositories'); 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.'); throw new Error('Should load `TenancyMiddlware` before this middleware.');
} }
Logger.info('[subscription_middleware] trying get tenant main subscription.'); 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. // Validate in case there is no any already subscription.
if (!subscription) { if (!subscription) {
Logger.info('[subscription_middleware] tenant has no subscription.', { tenantId }); Logger.info('[subscription_middleware] tenant has no subscription.', {
return res.boom.badRequest( tenantId,
'Tenant has no subscription.', });
{ errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }] } return res.boom.badRequest('Tenant has no subscription.', {
); errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
});
} }
// Validate in case the subscription is inactive. // Validate in case the subscription is inactive.
else if (subscription.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, { return res.boom.badRequest(null, {
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }], errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
}); });
} }
next(); next();
}; };

View File

@@ -1,5 +1,3 @@
import logger from "src/loaders/logger";
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';

View File

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

View File

@@ -1,6 +1,6 @@
import { Model } from 'objection';
export interface ISystemUser extends Model {
export interface ISystemUser {
id: number, id: number,
firstName: string, firstName: string,
lastName: string, lastName: string,
@@ -34,4 +34,12 @@ export interface IInviteUserInput {
lastName: string, lastName: string,
phoneNumber: string, phoneNumber: string,
password: string, password: string,
};
export interface IUserInvite {
id: number,
email: string,
token: string,
tenantId: number,
createdAt?: Date,
} }

View File

@@ -1,4 +1,4 @@
import { cloneDeep, cloneDeepWith, forOwn, isString } from 'lodash'; import { cloneDeep, forOwn, isString } from 'lodash';
import ModelEntityNotFound from 'exceptions/ModelEntityNotFound'; import ModelEntityNotFound from 'exceptions/ModelEntityNotFound';
export default class EntityRepository { export default class EntityRepository {
@@ -38,8 +38,7 @@ export default class EntityRepository {
* @returns {Promise<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute * @returns {Promise<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/ */
find(attributeValues = {}, withRelations?) { find(attributeValues = {}, withRelations?) {
return this.model return this.model.query()
.query()
.where(attributeValues) .where(attributeValues)
.withGraphFetched(withRelations); .withGraphFetched(withRelations);
} }

View File

@@ -23,7 +23,6 @@ import AuthenticationMailMessages from 'services/Authentication/AuthenticationMa
import AuthenticationSMSMessages from 'services/Authentication/AuthenticationSMSMessages'; import AuthenticationSMSMessages from 'services/Authentication/AuthenticationSMSMessages';
import TenantsManager from 'services/Tenancy/TenantsManager'; import TenantsManager from 'services/Tenancy/TenantsManager';
const ERRORS = { const ERRORS = {
INVALID_DETAILS: 'INVALID_DETAILS', INVALID_DETAILS: 'INVALID_DETAILS',
USER_INACTIVE: 'USER_INACTIVE', USER_INACTIVE: 'USER_INACTIVE',
@@ -32,7 +31,7 @@ const ERRORS = {
USER_NOT_FOUND: 'USER_NOT_FOUND', USER_NOT_FOUND: 'USER_NOT_FOUND',
TOKEN_EXPIRED: 'TOKEN_EXPIRED', TOKEN_EXPIRED: 'TOKEN_EXPIRED',
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS', PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
EMAIL_EXISTS: 'EMAIL_EXISTS' EMAIL_EXISTS: 'EMAIL_EXISTS',
}; };
@Service() @Service()
export default class AuthenticationService implements IAuthenticationService { export default class AuthenticationService implements IAuthenticationService {
@@ -136,6 +135,7 @@ export default class AuthenticationService implements IAuthenticationService {
*/ */
private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) { private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
const isEmailExists = await systemUserRepository.findOneByEmail( const isEmailExists = await systemUserRepository.findOneByEmail(
registerDTO.email registerDTO.email
); );
@@ -279,7 +279,10 @@ export default class AuthenticationService implements IAuthenticationService {
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
this.logger.info('[reset_password] saving a new hashed 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. // Deletes the used token.
await this.deletePasswordResetToken(tokenModel.email); await this.deletePasswordResetToken(tokenModel.email);

View File

@@ -12,13 +12,14 @@ import { hashPassword } from 'utils';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import InviteUsersMailMessages from 'services/InviteUsers/InviteUsersMailMessages'; import InviteUsersMailMessages from 'services/InviteUsers/InviteUsersMailMessages';
import events from 'subscribers/events'; import events from 'subscribers/events';
import { ISystemUser, IInviteUserInput } from 'interfaces'; import { ISystemUser, IInviteUserInput, IUserInvite } from 'interfaces';
import TenantsManagerService from 'services/Tenancy/TenantsManager'; import TenantsManagerService from 'services/Tenancy/TenantsManager';
const ERRORS = { const ERRORS = {
EMAIL_ALREADY_INVITED: 'EMAIL_ALREADY_INVITED', EMAIL_ALREADY_INVITED: 'EMAIL_ALREADY_INVITED',
INVITE_TOKEN_INVALID: 'INVITE_TOKEN_INVALID', INVITE_TOKEN_INVALID: 'INVITE_TOKEN_INVALID',
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS' PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
EMAIL_EXISTS: 'EMAIL_EXISTS'
}; };
@Service() @Service()
export default class InviteUserService { export default class InviteUserService {
@@ -66,12 +67,16 @@ export default class InviteUserService {
const user = await systemUserRepository.findOneByEmail(inviteToken.email); const user = await systemUserRepository.findOneByEmail(inviteToken.email);
// Sets the invited user details after invite accepting. // Sets the invited user details after invite accepting.
const updateUserOper = systemUserRepository.update({ const systemUserOper = systemUserRepository.create(
...inviteUserInput, {
active: 1, ...inviteUserInput,
inviteAcceptedAt: moment().format('YYYY-MM-DD'), email: inviteToken.email,
password: hashedPassword, tenantId: inviteToken.tenantId,
}, { id: user.id }); active: 1,
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
password: hashedPassword,
},
);
this.logger.info('[accept_invite] trying to delete the given token.'); this.logger.info('[accept_invite] trying to delete the given token.');
const deleteInviteTokenOper = Invite.query() const deleteInviteTokenOper = Invite.query()
@@ -79,14 +84,14 @@ export default class InviteUserService {
.delete(); .delete();
// Await all async operations. // Await all async operations.
const [updatedUser] = await Promise.all([ const [systemUser] = await Promise.all([
updateUserOper, systemUserOper,
deleteInviteTokenOper, deleteInviteTokenOper,
]); ]);
// Triggers `onUserAcceptInvite` event. // Triggers `onUserAcceptInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, { this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, {
inviteToken, inviteToken,
user: updatedUser, user: systemUser,
}); });
} }
@@ -96,21 +101,21 @@ export default class InviteUserService {
* @param {string} email - * @param {string} email -
* @param {IUser} authorizedUser - * @param {IUser} authorizedUser -
* *
* @return {Promise<IInvite>} * @return {Promise<IUserInvite>}
*/ */
public async sendInvite( public async sendInvite(
tenantId: number, tenantId: number,
email: string, email: string,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<{ ): Promise<{
invite: IInvite, invite: IUserInvite;
user: ISystemUser
}> { }> {
const { systemUserRepository } = this.sysRepositories;
// Throw error in case user email exists. // Throw error in case user email exists.
await this.throwErrorIfUserEmailExists(email); 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.'); this.logger.info('[send_invite] trying to store invite token.');
const invite = await Invite.query().insert({ const invite = await Invite.query().insert({
email, email,
@@ -121,16 +126,13 @@ export default class InviteUserService {
this.logger.info( this.logger.info(
'[send_invite] trying to store user with email and tenant.' '[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. // Triggers `onUserSendInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.sendInvite, { 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( public async checkInvite(
token: string token: string
): Promise<{ inviteToken: string; orgName: object }> { ): Promise<{ inviteToken: IUserInvite; orgName: object }> {
const inviteToken = await this.getInviteOrThrowError(token); const inviteToken = await this.getInviteOrThrowError(token);
// Find the tenant that associated to the given token. // Find the tenant that associated to the given token.
@@ -170,14 +172,27 @@ export default class InviteUserService {
*/ */
private async throwErrorIfUserEmailExists( private async throwErrorIfUserEmailExists(
email: string email: string
): Promise<ISystemUser> { ): Promise<void> {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.findOneByEmail(email); const foundUser = await systemUserRepository.findOneByEmail(email);
if (foundUser) { 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<void> {
const inviteToken = await Invite.query().findOne('email', email);
if (inviteToken) {
throw new ServiceError(ERRORS.EMAIL_ALREADY_INVITED); throw new ServiceError(ERRORS.EMAIL_ALREADY_INVITED);
} }
return foundUser;
} }
/** /**
@@ -186,7 +201,7 @@ export default class InviteUserService {
* @throws {ServiceError} * @throws {ServiceError}
* @returns {Invite} * @returns {Invite}
*/ */
private async getInviteOrThrowError(token: string) { private async getInviteOrThrowError(token: string): Promise<IUserInvite> {
const inviteToken = await Invite.query().findOne('token', token); const inviteToken = await Invite.query().findOne('token', token);
if (!inviteToken) { if (!inviteToken) {

View File

@@ -1,9 +1,17 @@
import { Inject, Service } from 'typedi';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { SystemUser } from 'system/models'; import { Inject, Service } from 'typedi';
import { ServiceError, ServiceErrors } from 'exceptions'; import { ServiceError, ServiceErrors } from 'exceptions';
import { ISystemUser, ISystemUserDTO } from 'interfaces'; 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() @Service()
export default class UsersService { export default class UsersService {
@@ -23,7 +31,7 @@ export default class UsersService {
* @param {IUserDTO} userDTO * @param {IUserDTO} userDTO
* @return {Promise<ISystemUser>} * @return {Promise<ISystemUser>}
*/ */
async editUser( public async editUser(
tenantId: number, tenantId: number,
userId: number, userId: number,
userDTO: ISystemUserDTO userDTO: ISystemUserDTO
@@ -36,49 +44,24 @@ export default class UsersService {
}); });
const userByPhoneNumber = await systemUserRepository.findOne({ const userByPhoneNumber = await systemUserRepository.findOne({
phoneNumber: userDTO.phoneNumber, phoneNumber: userDTO.phoneNumber,
id: userId id: userId,
}); });
const serviceErrors: ServiceError[] = []; const serviceErrors: ServiceError[] = [];
if (userByEmail) { if (userByEmail) {
serviceErrors.push(new ServiceError('email_already_exists')); serviceErrors.push(new ServiceError(ERRORS.EMAIL_ALREADY_EXISTS));
} }
if (userByPhoneNumber) { if (userByPhoneNumber) {
serviceErrors.push(new ServiceError('phone_number_already_exist')); serviceErrors.push(new ServiceError(ERRORS.PHONE_NUMBER_ALREADY_EXIST));
} }
if (serviceErrors.length > 0) { if (serviceErrors.length > 0) {
throw new ServiceErrors(serviceErrors); throw new ServiceErrors(serviceErrors);
} }
const updateSystemUser = await systemUserRepository const updateSystemUser = await systemUserRepository.update(
.update({ ...userDTO, }, { id: userId }); { ...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<ISystemUser> {
const { systemUserRepository } = this.repositories;
const user = await systemUserRepository.findOneByIdAndTenant(
userId,
tenantId
); );
return updateSystemUser;
if (!user) {
this.logger.info('[users] the given user not found.', {
tenantId,
userId,
});
throw new ServiceError('user_not_found');
}
return user;
} }
/** /**
@@ -86,14 +69,20 @@ export default class UsersService {
* @param {number} tenantId * @param {number} tenantId
* @param {number} userId * @param {number} userId
*/ */
async deleteUser(tenantId: number, userId: number): Promise<void> { public async deleteUser(tenantId: number, userId: number): Promise<void> {
const { systemUserRepository } = this.repositories; 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.', { this.logger.info('[users] trying to delete the given user.', {
tenantId, tenantId,
userId, userId,
}); });
// Validate the delete user should not be the last user.
await this.validateNotLastUserDelete(tenantId);
// Delete user from the storage.
await systemUserRepository.deleteById(userId); await systemUserRepository.deleteById(userId);
this.logger.info('[users] the given user deleted successfully.', { this.logger.info('[users] the given user deleted successfully.', {
@@ -104,18 +93,24 @@ export default class UsersService {
/** /**
* Activate the given user id. * Activate the given user id.
* @param {number} tenantId * @param {number} tenantId - Tenant id.
* @param {number} userId * @param {number} userId - User id.
* @return {Promise<void>}
*/ */
async activateUser( public async activateUser(
tenantId: number, tenantId: number,
userId: number, userId: number,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<void> { ): Promise<void> {
this.throwErrorIfUserIdSameAuthorizedUser(userId, authorizedUser);
const { systemUserRepository } = this.repositories; 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); const user = await this.getUserOrThrowError(tenantId, userId);
// Throw serivce error if the user is already activated.
this.throwErrorIfUserActive(user); this.throwErrorIfUserActive(user);
await systemUserRepository.activateUser(userId); await systemUserRepository.activateUser(userId);
@@ -127,15 +122,20 @@ export default class UsersService {
* @param {number} userId * @param {number} userId
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async inactivateUser( public async inactivateUser(
tenantId: number, tenantId: number,
userId: number, userId: number,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<void> { ): Promise<void> {
this.throwErrorIfUserIdSameAuthorizedUser(userId, authorizedUser);
const { systemUserRepository } = this.repositories; 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); const user = await this.getUserOrThrowError(tenantId, userId);
// Throw serivce error if the user is already inactivated.
this.throwErrorIfUserInactive(user); this.throwErrorIfUserInactive(user);
await systemUserRepository.inactivateById(userId); await systemUserRepository.inactivateById(userId);
@@ -146,10 +146,10 @@ export default class UsersService {
* @param {number} tenantId * @param {number} tenantId
* @param {object} filter * @param {object} filter
*/ */
async getList(tenantId: number) { public async getList(tenantId: number) {
const users = await SystemUser.query() const { systemUserRepository } = this.repositories;
.whereNotDeleted()
.where('tenant_id', tenantId); const users = await systemUserRepository.find({ tenantId });
return users; return users;
} }
@@ -159,18 +159,58 @@ export default class UsersService {
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {number} userId - User id. * @param {number} userId - User id.
*/ */
async getUser(tenantId: number, userId: number) { public async getUser(tenantId: number, userId: number) {
return this.getUserOrThrowError(tenantId, userId); 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<ISystemUser> {
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. * Throws service error in case the user was already active.
* @param {ISystemUser} user * @param {ISystemUser} user
* @throws {ServiceError} * @throws {ServiceError}
*/ */
throwErrorIfUserActive(user: ISystemUser) { private throwErrorIfUserActive(user: ISystemUser) {
if (user.active) { 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 * @param {ISystemUser} user
* @throws {ServiceError} * @throws {ServiceError}
*/ */
throwErrorIfUserInactive(user: ISystemUser) { private throwErrorIfUserInactive(user: ISystemUser) {
if (!user.active) { 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 {number} userId
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
*/ */
throwErrorIfUserIdSameAuthorizedUser( private throwErrorIfUserSameAuthorizedUser(
userId: number, userId: number,
authorizedUser: ISystemUser authorizedUser: ISystemUser
) { ) {
if (userId === authorizedUser.id) { if (userId === authorizedUser.id) {
throw new ServiceError('user_same_the_authorized_user'); throw new ServiceError(ERRORS.USER_SAME_THE_AUTHORIZED_USER);
} }
} }
} }

View File

@@ -59,7 +59,6 @@ export default class SaleInvoiceSubscriber {
); );
} }
/** /**
* Handles customer balance decrement once sale invoice deleted. * Handles customer balance decrement once sale invoice deleted.
*/ */

View File

@@ -4,8 +4,8 @@ exports.up = function (knex) {
table.increments(); table.increments();
table.string('first_name'); table.string('first_name');
table.string('last_name'); table.string('last_name');
table.string('email').unique().index(); table.string('email').index();
table.string('phone_number').unique().index(); table.string('phone_number').index();
table.string('password'); table.string('password');
table.boolean('active').index(); table.boolean('active').index();
table.string('language'); table.string('language');

View File

@@ -1,14 +1,9 @@
import { Model, mixin } from 'objection'; import { Model } from 'objection';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import SoftDelete from 'objection-soft-delete';
import SystemModel from 'system/models/SystemModel'; import SystemModel from 'system/models/SystemModel';
import moment from 'moment'; import SoftDeleteQueryBuilder from 'collection/SoftDeleteQueryBuilder';
export default class SystemUser extends mixin(SystemModel, [SoftDelete({ export default class SystemUser extends SystemModel {
columnName: 'deleted_at',
deletedValue: moment().format('YYYY-MM-DD HH:mm:ss'),
notDeletedValue: null,
})]) {
/** /**
* Table name. * Table name.
*/ */
@@ -16,6 +11,13 @@ export default class SystemUser extends mixin(SystemModel, [SoftDelete({
return 'users'; return 'users';
} }
/**
* Soft delete query builder.
*/
static get QueryBuilder() {
return SoftDeleteQueryBuilder;
}
/** /**
* Timestamps columns. * Timestamps columns.
*/ */
@@ -23,10 +25,16 @@ export default class SystemUser extends mixin(SystemModel, [SoftDelete({
return ['createdAt', 'updatedAt']; return ['createdAt', 'updatedAt'];
} }
/**
* Virtual attributes.
*/
static get virtualAttributes() { static get virtualAttributes() {
return ['fullName']; return ['fullName'];
} }
/**
* Full name attribute.
*/
get fullName() { get fullName() {
return (this.firstName + ' ' + this.lastName).trim(); return (this.firstName + ' ' + this.lastName).trim();
} }

View File

@@ -22,7 +22,6 @@ export default class SystemUserRepository extends SystemRepository {
return this.cache.get(cacheKey, () => { return this.cache.get(cacheKey, () => {
return this.model.query() return this.model.query()
.whereNotDeleted()
.findOne('email', crediential) .findOne('email', crediential)
.orWhere('phone_number', crediential); .orWhere('phone_number', crediential);
}); });
@@ -39,7 +38,6 @@ export default class SystemUserRepository extends SystemRepository {
return this.cache.get(cacheKey, () => { return this.cache.get(cacheKey, () => {
return this.model.query() return this.model.query()
.whereNotDeleted()
.findOne({ id: userId, tenant_id: tenantId }); .findOne({ id: userId, tenant_id: tenantId });
}); });
} }
@@ -53,7 +51,7 @@ export default class SystemUserRepository extends SystemRepository {
const cacheKey = this.getCacheKey('findOneByEmail', email); const cacheKey = this.getCacheKey('findOneByEmail', email);
return this.cache.get(cacheKey, () => { 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.cache.get(cacheKey, () => {
return this.model.query() return this.model.query()
.whereNotDeleted()
.findOne('phoneNumber', phoneNumber); .findOne('phoneNumber', phoneNumber);
}); });
} }