Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/src/common/data-injection.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,5 @@ export enum UseCaseType {
UPDATE_USERS_COMPANY_ROLES = 'UPDATE_USERS_COMPANY_ROLES',
DELETE_COMPANY = 'DELETE_COMPANY',
SUSPEND_USERS_IN_COMPANY = 'SUSPEND_USERS_IN_COMPANY',
UNSUSPEND_USERS_IN_COMPANY = 'UNSUSPEND_USERS_IN_COMPANY',
}
18 changes: 18 additions & 0 deletions backend/src/entities/company-info/company-info.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export class CompanyInfoController {
private readonly updateUses2faStatusInCompanyUseCase: IUpdateUsers2faStatusInCompany,
@Inject(UseCaseType.SUSPEND_USERS_IN_COMPANY)
private readonly suspendUsersInCompanyUseCase: ISuspendUsersInCompany,
@Inject(UseCaseType.UNSUSPEND_USERS_IN_COMPANY)
private readonly unSuspendUsersInCompanyUseCase: ISuspendUsersInCompany,
) {}

@ApiOperation({ summary: 'Get user company' })
Expand Down Expand Up @@ -401,4 +403,20 @@ export class CompanyInfoController {
): Promise<SuccessResponse> {
return await this.suspendUsersInCompanyUseCase.execute({ companyInfoId, usersEmails }, InTransactionEnum.ON);
}

@ApiOperation({ summary: 'Unsuspend users in company' })
@ApiResponse({
status: 200,
description: 'Users unsuspend.',
type: SuccessResponse,
})
@ApiBody({ type: SuspendUsersInCompanyDto })
@UseGuards(CompanyAdminGuard)
@Put('/users/unsuspend/:companyId')
async unSuspendUsersInCompany(
@Param('companyId') companyInfoId: string,
@Body() { usersEmails }: SuspendUsersInCompanyDto,
): Promise<SuccessResponse> {
return await this.unSuspendUsersInCompanyUseCase.execute({ companyInfoId, usersEmails }, InTransactionEnum.ON);
}
}
6 changes: 6 additions & 0 deletions backend/src/entities/company-info/company-info.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { DeleteCompanyUseCase } from './use-cases/delete-company-use-case.js';
import { CheckIsVerificationLinkAvailable } from './use-cases/check-verification-link.available.use.case.js';
import { UpdateUses2faStatusInCompanyUseCase } from './use-cases/update-uses-2fa-status-in-company.use.case.js';
import { SuspendUsersInCompanyUseCase } from './use-cases/suspend-users-in-company.use.case.js';
import { UnsuspendUsersInCompanyUseCase } from './use-cases/unsuspend-users-in-company.use.case.js';

@Module({
imports: [
Expand Down Expand Up @@ -110,6 +111,10 @@ import { SuspendUsersInCompanyUseCase } from './use-cases/suspend-users-in-compa
provide: UseCaseType.SUSPEND_USERS_IN_COMPANY,
useClass: SuspendUsersInCompanyUseCase,
},
{
provide: UseCaseType.UNSUSPEND_USERS_IN_COMPANY,
useClass: UnsuspendUsersInCompanyUseCase,
},
],
controllers: [CompanyInfoController],
})
Expand All @@ -129,6 +134,7 @@ export class CompanyInfoModule implements NestModule {
{ path: '/company/users/roles/:companyId', method: RequestMethod.PUT },
{ path: 'company/2fa/:companyId', method: RequestMethod.PUT },
{ path: '/company/users/suspend/:companyId', method: RequestMethod.PUT },
{ path: '/company/users/unsuspend/:companyId', method: RequestMethod.PUT },
);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BadRequestException, Inject, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { BadRequestException, Inject, Injectable, InternalServerErrorException, NotFoundException, Scope } from '@nestjs/common';
import AbstractUseCase from '../../../common/abstract-use.case.js';
import { SuccessResponse } from '../../../microservices/saas-microservice/data-structures/common-responce.ds.js';
import { SuspendUsersInCompanyDS } from '../application/data-structures/suspend-users-in-company.ds.js';
Expand All @@ -9,6 +9,7 @@ import { SaasCompanyGatewayService } from '../../../microservices/gateways/saas-
import { Messages } from '../../../exceptions/text/messages.js';
import { isSaaS } from '../../../helpers/app/is-saas.js';

@Injectable({ scope: Scope.REQUEST })
export class SuspendUsersInCompanyUseCase
extends AbstractUseCase<SuspendUsersInCompanyDS, SuccessResponse>
implements ISuspendUsersInCompany
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
BadRequestException,
HttpException,
HttpStatus,
Inject,
Injectable,
InternalServerErrorException,
NotFoundException,
Scope,
} from '@nestjs/common';
import AbstractUseCase from '../../../common/abstract-use.case.js';
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
import { BaseType } from '../../../common/data-injection.tokens.js';
import { SaasCompanyGatewayService } from '../../../microservices/gateways/saas-gateway.ts/saas-company-gateway.service.js';
import { SuccessResponse } from '../../../microservices/saas-microservice/data-structures/common-responce.ds.js';
import { SuspendUsersInCompanyDS } from '../application/data-structures/suspend-users-in-company.ds.js';
import { ISuspendUsersInCompany } from './company-info-use-cases.interface.js';
import { Messages } from '../../../exceptions/text/messages.js';
import { isSaaS } from '../../../helpers/app/is-saas.js';

@Injectable({ scope: Scope.REQUEST })
export class UnsuspendUsersInCompanyUseCase
extends AbstractUseCase<SuspendUsersInCompanyDS, SuccessResponse>
implements ISuspendUsersInCompany
{
constructor(
@Inject(BaseType.GLOBAL_DB_CONTEXT)
protected _dbContext: IGlobalDatabaseContext,
private readonly saasCompanyGatewayService: SaasCompanyGatewayService,
) {
super();
}

protected async implementation(inputData: SuspendUsersInCompanyDS): Promise<SuccessResponse> {
const { companyInfoId, usersEmails } = inputData;
const foundCompany = await this._dbContext.companyInfoRepository.findCompanyInfoWithUsersById(companyInfoId);
if (!foundCompany) {
throw new NotFoundException(Messages.COMPANY_NOT_FOUND);
}
const userIdsToSuspend = foundCompany.users
.filter((user) => usersEmails.includes(user.email))
.map((user) => user.id);

if (!userIdsToSuspend.length) {
throw new BadRequestException(Messages.NO_USERS_TO_SUSPEND);
}

if (isSaaS()) {
const canInviteMoreUsers = await this.saasCompanyGatewayService.canInviteMoreUsers(companyInfoId);
if (!canInviteMoreUsers) {
throw new HttpException(
{
message: Messages.CANT_UNSUSPEND_USERS_FREE_PLAN,
},
HttpStatus.BAD_REQUEST,
);
}
const { success } = await this.saasCompanyGatewayService.unSuspendUsersInCompany(companyInfoId, userIdsToSuspend);
if (!success) {
throw new InternalServerErrorException(Messages.SAAS_SUSPEND_USERS_FAILED_UNHANDLED_ERROR);
}
}
await this._dbContext.userRepository.unSuspendUsers(userIdsToSuspend);
return {
success: true,
};
}
}
2 changes: 2 additions & 0 deletions backend/src/exceptions/text/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export const Messages = {
SAAS_DELETE_COMPANY_FAILED_UNHANDLED_ERROR: `Failed to delete company in SaaS. Please contact our support team.`,
SAAS_UPDATE_2FA_STATUS_FAILED_UNHANDLED_ERROR: `Failed to update 2fa status in SaaS. Please contact our support team.`,
SAAS_SUSPEND_USERS_FAILED_UNHANDLED_ERROR: `Failed to suspend users in SaaS. Please contact our support team.`,
SAAS_UNSUSPEND_USERS_FAILED_UNHANDLED_ERROR: `Failed to unsuspend users in SaaS. Please contact our support team.`,
SOMETHING_WENT_WRONG_ROW_ADD: 'Something went wrong on row insertion, check inserted parameters and try again',
SSH_FORMAT_INCORRECT: 'Ssh value must be a boolean',
SSH_HOST_MISSING: 'Ssh host is missing',
Expand Down Expand Up @@ -309,6 +310,7 @@ export const Messages = {
MAXIMUM_FREE_INVITATION_REACHED_CANNOT_BE_INVITED_IN_COMPANY:
'Sorry you can not join this company because reached maximum number of users for free plan. Please ask you connection owner to upgrade plan or delete unused user accounts from company',
MAXIMUM_INVITATIONS_COUNT_REACHED_CANT_INVITE: ` Sorry, the maximum number of of users for free plan has been reached. You can't invite more users. Please ask you connection owner to upgrade plan or delete unused user accounts from company, or revoke unaccepted invitations.`,
CANT_UNSUSPEND_USERS_FREE_PLAN: `You can't unsuspend users because reached maximum number of users for free plan. Please ask you connection owner to upgrade plan or delete unused/suspended user accounts from company, or revoke unaccepted invitations.`,
FAILED_CREATE_SUBSCRIPTION_LOG: 'Failed to create subscription log. Please contact our support team.',
FAILED_CREATE_SUBSCRIPTION_LOG_YOUR_CUSTOMER_IS_DELETED: `Failed to create subscription log. Your customer is deleted. Please contact our support team.`,
URL_INVALID: `Url is invalid`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,28 @@ export class SaasCompanyGatewayService extends BaseSaasGatewayService {
return null;
}

public async unSuspendUsersInCompany(companyId: string, userIds: Array<string>): Promise<SuccessResponse | null> {
const result = await this.sendRequestToSaaS(`/webhook/company/unsuspend/users`, 'POST', {
companyId,
userIds,
});
if (result.status > 299) {
throw new HttpException(
{
message: Messages.SAAS_UNSUSPEND_USERS_FAILED_UNHANDLED_ERROR,
originalMessage: result?.body?.message ? result.body.message : undefined,
},
result.status,
);
}
if (!isObjectEmpty(result.body)) {
return {
success: result.body.success as boolean,
};
}
return null;
}

private isDataFoundSassCompanyInfoDS(data: unknown): data is FoundSassCompanyInfoDS {
return (
typeof data === 'object' &&
Expand Down
105 changes: 105 additions & 0 deletions backend/test/ava-tests/saas-tests/company-info-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,3 +835,108 @@ test(`${currentTest} should suspend users in company`, async (t) => {
const suspendUsersCount = usersAfterSuspend.filter((user: any) => user.suspended).length;
t.is(suspendUsersCount, 5);
});

currentTest = `PUT /company/users/unsuspend/:companyId`;
test(`${currentTest} should suspend users in company`, async (t) => {
const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app);
const {
connections,
firstTableInfo,
groups,
permissions,
secondTableInfo,
users: { adminUserToken, simpleUserToken, adminUserEmail, simpleUserEmail, simpleUserPassword },
} = testData;

const foundCompanyInfo = await request(app.getHttpServer())
.get('/company/my/full')
.set('Content-Type', 'application/json')
.set('Cookie', adminUserToken)
.set('Accept', 'application/json');

t.is(foundCompanyInfo.status, 200);
const foundCompanyInfoRO = JSON.parse(foundCompanyInfo.text);

let firstConnection = foundCompanyInfoRO.connections.find((connectionRO) => connections.firstId === connectionRO.id);
const createdGroup = firstConnection.groups.find((groupRO) => groupRO.id === groups.createdGroupId);

const additionalUsers: Array<{
email: string;
password: string;
token: string;
}> = [];
for (let i = 0; i < 5; i++) {
const invitationResult = await inviteUserInCompanyAndGroupAndAcceptInvitation(
adminUserToken,
'USER',
createdGroup.id,
app,
);
additionalUsers.push(invitationResult);
}
const foundCompanyInfoWithAddedUsers = await request(app.getHttpServer())
.get('/company/my/full')
.set('Content-Type', 'application/json')
.set('Cookie', adminUserToken)
.set('Accept', 'application/json');

t.is(foundCompanyInfo.status, 200);
const foundCompanyInfoWithAddedUsersRO = JSON.parse(foundCompanyInfoWithAddedUsers.text);
firstConnection = foundCompanyInfoWithAddedUsersRO.connections.find(
(connectionRO) => connections.firstId === connectionRO.id,
);
const { users } = firstConnection.groups.find((groupRO) => groupRO.id === groups.createdGroupId);
users.forEach((user: any) => {
t.is(user.suspended, false);
});

const suspendUsersResult = await request(app.getHttpServer())
.put(`/company/users/suspend/${foundCompanyInfoRO.id}`)
.send({
usersEmails: additionalUsers.map((user: any) => user.email),
})
.set('Content-Type', 'application/json')
.set('Cookie', adminUserToken)
.set('Accept', 'application/json');

t.is(suspendUsersResult.status, 200);

const foundCompanyInfoAfterSuspend = await request(app.getHttpServer())
.get('/company/my/full')
.set('Content-Type', 'application/json')
.set('Cookie', adminUserToken)
.set('Accept', 'application/json');

const foundCompanyInfoAfterSuspendRO = JSON.parse(foundCompanyInfoAfterSuspend.text);
firstConnection = foundCompanyInfoAfterSuspendRO.connections.find(
(connectionRO) => connections.firstId === connectionRO.id,
);
const { users: usersAfterSuspend } = firstConnection.groups.find((groupRO) => groupRO.id === groups.createdGroupId);
const suspendUsersCount = usersAfterSuspend.filter((user: any) => user.suspended).length;
t.is(suspendUsersCount, 5);

const unsuspendUsersResult = await request(app.getHttpServer())
.put(`/company/users/unsuspend/${foundCompanyInfoRO.id}`)
.send({
usersEmails: additionalUsers.map((user: any) => user.email),
})
.set('Content-Type', 'application/json')
.set('Cookie', adminUserToken)
.set('Accept', 'application/json');

t.is(unsuspendUsersResult.status, 200);

const foundCompanyInfoAfterUnsuspend = await request(app.getHttpServer())
.get('/company/my/full')
.set('Content-Type', 'application/json')
.set('Cookie', adminUserToken)
.set('Accept', 'application/json');

const foundCompanyInfoAfterUnsuspendRO = JSON.parse(foundCompanyInfoAfterUnsuspend.text);
firstConnection = foundCompanyInfoAfterUnsuspendRO.connections.find(
(connectionRO) => connections.firstId === connectionRO.id,
);
const { users: usersAfterUnsuspend } = firstConnection.groups.find((groupRO) => groupRO.id === groups.createdGroupId);
const unsuspendUsersCount = usersAfterUnsuspend.filter((user: any) => !user.suspended).length;
t.is(unsuspendUsersCount, 7);
});