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
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ import { InvitationInCompanyEntity } from '../../entities/company-info/invitatio
import { IInvitationInCompanyRepository } from '../../entities/company-info/invitation-in-company/repository/invitation-repository.interface.js';
import { IUserSessionSettings } from '../../entities/user/user-session-settings/reposiotory/user-session-settings-repository.interface.js';
import { UserSessionSettingsEntity } from '../../entities/user/user-session-settings/user-session-settings.entity.js';
import { UserEntity } from '../../entities/user/user.entity.js';
import { ConnectionEntity } from '../../entities/connection/connection.entity.js';

export interface IGlobalDatabaseContext extends IDatabaseContext {
userRepository: IUserRepository;
connectionRepository: IConnectionRepository;
userRepository: Repository<UserEntity> & IUserRepository;
connectionRepository: Repository<ConnectionEntity> & IConnectionRepository;
groupRepository: IGroupRepository;
permissionRepository: IPermissionRepository;
tableSettingsRepository: ITableSettingsRepository;
Expand Down
8 changes: 4 additions & 4 deletions backend/src/common/application/global-database-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ import { IUserSessionSettings } from '../../entities/user/user-session-settings/
export class GlobalDatabaseContext implements IGlobalDatabaseContext {
private _queryRunner: QueryRunner;

private _userRepository: IUserRepository;
private _connectionRepository: IConnectionRepository;
private _userRepository: Repository<UserEntity> & IUserRepository;
private _connectionRepository: Repository<ConnectionEntity> & IConnectionRepository;
private _groupRepository: IGroupRepository;
private _permissionRepository: IPermissionRepository;
private _tableSettingsRepository: ITableSettingsRepository;
Expand Down Expand Up @@ -169,11 +169,11 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext {
.extend(userSessionSettingsRepositoryExtension);
}

public get userRepository(): IUserRepository {
public get userRepository(): Repository<UserEntity> & IUserRepository {
return this._userRepository;
}

public get connectionRepository(): IConnectionRepository {
public get connectionRepository(): Repository<ConnectionEntity> & IConnectionRepository {
return this._connectionRepository;
}

Expand Down
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 @@ -127,4 +127,5 @@ export enum UseCaseType {
REVOKE_INVITATION_IN_COMPANY = 'REVOKE_INVITATION_IN_COMPANY',
UPDATE_COMPANY_NAME = 'UPDATE_COMPANY_NAME',
UPDATE_USERS_COMPANY_ROLES = 'UPDATE_USERS_COMPANY_ROLES',
DELETE_COMPANY = 'DELETE_COMPANY',
}
38 changes: 36 additions & 2 deletions backend/src/entities/company-info/company-info.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Messages } from '../../exceptions/text/messages.js';
import { UseCaseType } from '../../common/data-injection.tokens.js';
import { SlugUuid } from '../../decorators/slug-uuid.decorator.js';
import {
IDeleteCompany,
IGetCompanyName,
IGetUserCompany,
IGetUserEmailCompanies,
Expand Down Expand Up @@ -86,6 +87,8 @@ export class CompanyInfoController {
private readonly updateCompanyNameUseCase: IUpdateCompanyName,
@Inject(UseCaseType.UPDATE_USERS_COMPANY_ROLES)
private readonly updateUsersCompanyRolesUseCase: IUpdateUsersCompanyRoles,
@Inject(UseCaseType.DELETE_COMPANY)
private readonly deleteCompanyUseCase: IDeleteCompany,
) {}

@ApiOperation({ summary: 'Get user company' })
Expand Down Expand Up @@ -266,7 +269,10 @@ export class CompanyInfoController {
})
@UseGuards(CompanyAdminGuard)
@Put('/name/:slug')
async updateCompanyName(@SlugUuid() companyId: string, @Body() nameData: UpdateCompanyNameDto) {
async updateCompanyName(
@SlugUuid() companyId: string,
@Body() nameData: UpdateCompanyNameDto,
): Promise<SuccessResponse> {
const { name } = nameData;
return await this.updateCompanyNameUseCase.execute({ name, companyId }, InTransactionEnum.ON);
}
Expand All @@ -280,8 +286,36 @@ export class CompanyInfoController {
})
@UseGuards(CompanyAdminGuard)
@Put('/users/roles/:companyId')
async updateUsersRoles(@Param('companyId') companyId: string, @Body() usersAndRoles: UpdateUsersRolesRequestDto) {
async updateUsersRoles(
@Param('companyId') companyId: string,
@Body() usersAndRoles: UpdateUsersRolesRequestDto,
): Promise<SuccessResponse> {
const { users } = usersAndRoles;
return await this.updateUsersCompanyRolesUseCase.execute({ users, companyId }, InTransactionEnum.ON);
}

@ApiOperation({ summary: 'Delete company' })
@ApiResponse({
status: 200,
description: 'Company was deleted.',
type: SuccessResponse,
})
@UseGuards(CompanyAdminGuard)
@Delete('/my')
async deleteCompany(
@UserId() userId: string,
@Res({ passthrough: true }) response: Response,
): Promise<SuccessResponse> {
const deleteResult = await this.deleteCompanyUseCase.execute(userId, InTransactionEnum.OFF);

response.cookie(Constants.JWT_COOKIE_KEY_NAME, '', {
...getCookieDomainOptions(),
});
response.cookie(Constants.ROCKETADMIN_AUTHENTICATED_COOKIE, 1, {
expires: new Date(0),
httpOnly: false,
...getCookieDomainOptions(),
});
return deleteResult;
}
}
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 @@ -25,6 +25,7 @@ import { RevokeUserInvitationInCompanyUseCase } from './use-cases/revoke-invitat
import { UpdateCompanyNameUseCase } from './use-cases/update-company-name.use.case.js';
import { GetCompanyNameUseCase } from './use-cases/get-company-name.use.case.js';
import { UpdateUsersCompanyRolesUseCase } from './use-cases/update-users-company-roles.use.case.js';
import { DeleteCompanyUseCase } from './use-cases/delete-company-use-case.js';

@Module({
imports: [
Expand Down Expand Up @@ -90,6 +91,10 @@ import { UpdateUsersCompanyRolesUseCase } from './use-cases/update-users-company
provide: UseCaseType.UPDATE_USERS_COMPANY_ROLES,
useClass: UpdateUsersCompanyRolesUseCase,
},
{
provide: UseCaseType.DELETE_COMPANY,
useClass: DeleteCompanyUseCase,
},
],
controllers: [CompanyInfoController],
})
Expand All @@ -100,6 +105,7 @@ export class CompanyInfoModule implements NestModule {
.forRoutes(
{ path: '/company/user/:slug', method: RequestMethod.PUT },
{ path: '/company/my', method: RequestMethod.GET },
{ path: '/company/my', method: RequestMethod.DELETE },
{ path: 'company/my/full', method: RequestMethod.GET },
{ path: '/company/users/:slug', method: RequestMethod.GET },
{ path: '/company/:companyId/user/:userId', method: RequestMethod.DELETE },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {

async findFullCompanyInfoByUserId(userId: string): Promise<CompanyInfoEntity> {
return await this.createQueryBuilder('company_info')
.leftJoinAndSelect('company_info.users', 'current_user')
.leftJoinAndSelect('company_info.users', 'users')
.leftJoinAndSelect('company_info.connections', 'connections')
.leftJoinAndSelect('company_info.invitations', 'invitations')
.leftJoinAndSelect('connections.groups', 'groups')
.leftJoinAndSelect('connections.author', 'connection_author')
.leftJoinAndSelect('groups.users', 'groups_users')
.where('users.id = :userId', { userId })
.where('current_user.id = :userId', { userId })
.getOne();
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@ export interface IUpdateCompanyName {
export interface IUpdateUsersCompanyRoles {
execute(inputData: UpdateUsersCompanyRolesDs, inTransaction: InTransactionEnum): Promise<SuccessResponse>;
}

export interface IDeleteCompany {
execute(userId: string, inTransaction: InTransactionEnum): Promise<SuccessResponse>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Inject, InternalServerErrorException, NotFoundException } 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 { SuccessResponse } from '../../../microservices/saas-microservice/data-structures/common-responce.ds.js';
import { IDeleteCompany } from './company-info-use-cases.interface.js';
import { Messages } from '../../../exceptions/text/messages.js';
import { isSaaS } from '../../../helpers/app/is-saas.js';
import { SaasCompanyGatewayService } from '../../../microservices/gateways/saas-gateway.ts/saas-company-gateway.service.js';

export class DeleteCompanyUseCase extends AbstractUseCase<string, SuccessResponse> implements IDeleteCompany {
constructor(
@Inject(BaseType.GLOBAL_DB_CONTEXT)
protected _dbContext: IGlobalDatabaseContext,
private readonly saasCompanyGatewayService: SaasCompanyGatewayService,
) {
super();
}

protected async implementation(userId: string): Promise<SuccessResponse> {
let count = 0;
const foundFullCompany = await this._dbContext.companyInfoRepository.findFullCompanyInfoByUserId(userId);
console.log('🚀 ~ DeleteCompanyUseCase ~ implementation ~ foundFullCompany:', foundFullCompany);
if (!foundFullCompany) {
throw new NotFoundException(Messages.COMPANY_NOT_FOUND);
}
const { users, connections, invitations, id } = foundFullCompany;
if (isSaaS()) {
const deleteResult = await this.saasCompanyGatewayService.deleteCompany(id);
if (!deleteResult) {
throw new InternalServerErrorException(Messages.SAAS_DELETE_COMPANY_FAILED_UNHANDLED_ERROR);
}
}
if (users.length) {
await this._dbContext.userRepository.remove(users);
}
if (connections.length) {
await this._dbContext.connectionRepository.remove(connections);
}
if (invitations.length) {
await this._dbContext.invitationInCompanyRepository.remove(invitations);
}
await this._dbContext.companyInfoRepository.remove(foundFullCompany);
return {
success: true,
};
}
}
1 change: 1 addition & 0 deletions backend/src/exceptions/text/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export const Messages = {
ROW_PRIMARY_KEY_NOT_FOUND: 'Row with this primary key not found',
SAAS_COMPANY_NOT_REGISTERED_WITH_USER_INVITATION: `Failed to invite user in SaaS. Please contact our support team.`,
SAAS_UPDATE_USERS_ROLES_FAILED_UNHANDLED_ERROR: `Failed to update users roles in SaaS. Please contact our support team.`,
SAAS_DELETE_COMPANY_FAILED_UNHANDLED_ERROR: `Failed to delete company 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
9 changes: 1 addition & 8 deletions backend/src/guards/company-admin.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,7 @@ export class CompanyAdminGuard implements CanActivate {
companyId = foundCompanyInfo?.id;
}
if (!companyId || !validateUuidByRegex(companyId)) {
reject(
new HttpException(
{
message: 'Company id is missing',
},
HttpStatus.BAD_REQUEST,
),
);
resolve(false);
return;
}
let foundUser: UserEntity;
Expand Down
9 changes: 1 addition & 8 deletions backend/src/guards/company-user.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,7 @@ export class CompanyUserGuard implements CanActivate {
companyId = foundCompanyInfo?.id;
}
if (!companyId || !validateUuidByRegex(companyId)) {
reject(
new HttpException(
{
message: 'Company id is missing',
},
HttpStatus.BAD_REQUEST,
),
);
resolve(false);
return;
}
let foundUser: UserEntity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,25 @@ export class SaasCompanyGatewayService extends BaseSaasGatewayService {
return null;
}

public async deleteCompany(companyId: string): Promise<SuccessResponse | null> {
const result = await this.sendRequestToSaaS(`/webhook/company/${companyId}`, 'DELETE', null);
if (result.status > 299) {
throw new HttpException(
{
message: Messages.SAAS_DELETE_COMPANY_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
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,42 @@ test(`${currentTest} should update user roles in company`, async (t) => {
const foundUserAfterUpdate = usersInCompanyROAfterUpdate.find((user) => user.id === foundNonAdminUser.id);
t.is(foundUserAfterUpdate.role, 'ADMIN');
});

currentTest = `DELETE company`;

test(`${currentTest} should delete company`, async (t) => {
const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app);
const {
connections,
firstTableInfo,
groups,
permissions,
secondTableInfo,
users: { adminUserToken, simpleUserToken, adminUserEmail, simpleUserEmail },
} = 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 deleteCompanyResult = await request(app.getHttpServer())
.delete(`/company/my`)
.set('Content-Type', 'application/json')
.set('Cookie', adminUserToken)
.set('Accept', 'application/json');

t.is(deleteCompanyResult.status, 200);
const deleteCompanyResultRO = JSON.parse(deleteCompanyResult.text);
t.is(deleteCompanyResultRO.success, true);

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

t.is(foundCompanyInfoAfterDelete.status, 403);
});
39 changes: 39 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 @@ -538,3 +538,42 @@ test(`${currentTest} should update user roles in company`, async (t) => {
t.is(user.role, 'ADMIN');
}
});

currentTest = `DELETE company`;

test(`${currentTest} should delete company`, async (t) => {
const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app);
const {
connections,
firstTableInfo,
groups,
permissions,
secondTableInfo,
users: { adminUserToken, simpleUserToken, adminUserEmail, simpleUserEmail },
} = 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 deleteCompanyResult = await request(app.getHttpServer())
.delete(`/company/my`)
.set('Content-Type', 'application/json')
.set('Cookie', adminUserToken)
.set('Accept', 'application/json');

t.is(deleteCompanyResult.status, 200);
const deleteCompanyResultRO = JSON.parse(deleteCompanyResult.text);
t.is(deleteCompanyResultRO.success, true);

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

t.is(foundCompanyInfoAfterDelete.status, 403);
});