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/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"crc": "^4.3.2",
"crypto-js": "4.2.0",
"csv": "^6.3.5",
"docker-names": "^1.2.1",
"dotenv": "16.3.1",
"eslint-plugin-security": "1.7.1",
"express": "4.18.2",
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 @@ -128,6 +128,7 @@ export enum UseCaseType {
GET_USERS_IN_COMPANY = 'GET_USERS_IN_COMPANY',
REMOVE_USER_FROM_COMPANY = 'REMOVE_USER_FROM_COMPANY',
REVOKE_INVITATION_IN_COMPANY = 'REVOKE_INVITATION_IN_COMPANY',
UPDATE_COMPANY_NAME = 'UPDATE_COMPANY_NAME',
}

export enum DynamicModuleEnum {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class UpdateCompanyNameDS {
companyId: string;
name: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';

export class UpdateCompanyNameDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
@MaxLength(255)
name: string;
}
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 @@ -27,6 +27,7 @@ import {
IInviteUserInCompanyAndConnectionGroup,
IRemoveUserFromCompany,
IRevokeUserInvitationInCompany,
IUpdateCompanyName,
IVerifyInviteUserInCompanyAndConnectionGroup,
} from './use-cases/company-info-use-cases.interface.js';
import { ValidationHelper } from '../../helpers/validators/validation-helper.js';
Expand All @@ -49,6 +50,7 @@ import { SimpleFoundUserInfoDs } from '../user/application/data-structures/found
import { RemoveUserFromCompanyRequestDto } from './application/dto/remove-user-from-company-request.dto.js';
import { SuccessResponse } from '../../microservices/saas-microservice/data-structures/common-responce.ds.js';
import { RevokeInvitationRequestDto } from './application/dto/revoke-invitation-request.dto.js';
import { UpdateCompanyNameDto } from './application/dto/update-company-name.dto.js';

@UseInterceptors(SentryInterceptor)
@Controller('company')
Expand All @@ -73,6 +75,8 @@ export class CompanyInfoController {
private readonly removeUserFromCompanyUseCase: IRemoveUserFromCompany,
@Inject(UseCaseType.REVOKE_INVITATION_IN_COMPANY)
private readonly revokeInvitationInCompanyUseCase: IRevokeUserInvitationInCompany,
@Inject(UseCaseType.UPDATE_COMPANY_NAME)
private readonly updateCompanyNameUseCase: IUpdateCompanyName,
) {}

@ApiOperation({ summary: 'Get user company' })
Expand Down Expand Up @@ -226,4 +230,18 @@ export class CompanyInfoController {
isTemporary: tokenInfo.isTemporary,
};
}

@ApiOperation({ summary: 'Update company name' })
@ApiBody({ type: UpdateCompanyNameDto })
@ApiResponse({
status: 200,
description: 'Company name was updated.',
type: SuccessResponse,
})
@UseGuards(CompanyAdminGuard)
@Put('/name/:slug')
async updateCompanyName(@SlugUuid() companyId: string, @Body() nameData: UpdateCompanyNameDto) {
const { name } = nameData;
return await this.updateCompanyNameUseCase.execute({ name, companyId });
}
}
16 changes: 14 additions & 2 deletions backend/src/entities/company-info/company-info.entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Entity, OneToMany, PrimaryColumn, Relation } from 'typeorm';
import { BeforeInsert, Column, Entity, OneToMany, PrimaryColumn, Relation } from 'typeorm';
import { UserEntity } from '../user/user.entity.js';
import { ConnectionEntity } from '../connection/connection.entity.js';
import { InvitationInCompanyEntity } from './invitation-in-company/invitation-in-company.entity.js';
import DockerNames from 'docker-names';
import { nanoid } from 'nanoid';

@Entity('company_info')
export class CompanyInfoEntity {
Expand All @@ -13,12 +15,22 @@ export class CompanyInfoEntity {
})
users: Relation<UserEntity>[];

@Column({ type: 'varchar', length: 255, unique: true, nullable: true })
name: string;

@BeforeInsert()
getRandomName(): void {
if (!this.name) {
this.name = `${DockerNames.getRandomName()}_${nanoid(5)}`;
}
}

@OneToMany(() => ConnectionEntity, (connection) => connection.company, {
onDelete: 'NO ACTION',
})
connections: Relation<ConnectionEntity>[];

@OneToMany(()=> InvitationInCompanyEntity, (invitation) => invitation.company, {
@OneToMany(() => InvitationInCompanyEntity, (invitation) => invitation.company, {
onDelete: 'NO ACTION',
})
invitations: Relation<InvitationInCompanyEntity>[];
Expand Down
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 @@ -22,6 +22,7 @@ import { GetUserEmailCompaniesUseCase } from './use-cases/get-user-email-compani
import { GetAllUsersInCompanyUseCase } from './use-cases/get-all-users-in-company.use.case.js';
import { RemoveUserFromCompanyUseCase } from './use-cases/remove-user-from-company.use.case.js';
import { RevokeUserInvitationInCompanyUseCase } from './use-cases/revoke-invitation-in-company.use.case.js';
import { UpdateCompanyNameUseCase } from './use-cases/update-company-name.use.case.js';

@Module({
imports: [
Expand Down Expand Up @@ -75,6 +76,10 @@ import { RevokeUserInvitationInCompanyUseCase } from './use-cases/revoke-invitat
provide: UseCaseType.REVOKE_INVITATION_IN_COMPANY,
useClass: RevokeUserInvitationInCompanyUseCase,
},
{
provide: UseCaseType.UPDATE_COMPANY_NAME,
useClass: UpdateCompanyNameUseCase,
},
],
controllers: [CompanyInfoController],
})
Expand All @@ -89,6 +94,7 @@ export class CompanyInfoModule implements NestModule {
{ path: '/company/users/:slug', method: RequestMethod.GET },
{ path: '/company/user/remove/:slug', method: RequestMethod.PUT },
{ path: '/company/invitation/revoke/:slug', method: RequestMethod.PUT },
{ path: '/company/name/:slug', method: RequestMethod.PUT },
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { InviteUserInCompanyAndConnectionGroupDs } from '../application/data-structures/invite-user-in-company-and-connection-group.ds.js';
import { InvitedUserInCompanyAndConnectionGroupDs } from '../application/data-structures/invited-user-in-company-and-connection-group.ds.js';
import { RemoveUserFromCompanyDs } from '../application/data-structures/remove-user-from-company.ds.js';
import { UpdateCompanyNameDS } from '../application/data-structures/update-company-name.ds.js';

export interface IInviteUserInCompanyAndConnectionGroup {
execute(inputData: InviteUserInCompanyAndConnectionGroupDs): Promise<InvitedUserInCompanyAndConnectionGroupDs>;
Expand Down Expand Up @@ -42,3 +43,7 @@ export interface IRemoveUserFromCompany {
export interface IRevokeUserInvitationInCompany {
execute(inputData: RemoveUserFromCompanyDs): Promise<SuccessResponse>;
}

export interface IUpdateCompanyName {
execute(inputData: UpdateCompanyNameDS): Promise<SuccessResponse>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { HttpException, HttpStatus, Inject } 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 { UpdateCompanyNameDS } from '../application/data-structures/update-company-name.ds.js';
import { IUpdateCompanyName } from './company-info-use-cases.interface.js';
import { isSaaS } from '../../../helpers/app/is-saas.js';
import { Messages } from '../../../exceptions/text/messages.js';

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

protected async implementation(inputData: UpdateCompanyNameDS): Promise<SuccessResponse> {
const { companyId, name } = inputData;
const foundCompany = await this._dbContext.companyInfoRepository.findOneBy({ id: companyId });
foundCompany.name = name;
await this._dbContext.companyInfoRepository.save(foundCompany);

if (isSaaS()) {
const saasResponse = await this.saasCompanyGatewayService.updateCompanyName(companyId, name);
if (!saasResponse) {
throw new HttpException(
{
message: Messages.COMPANY_NAME_UPDATE_FAILED_UNHANDLED_ERROR,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
return {
success: true,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function buildFoundCompanyInfoDs(
if (!companyInfoFromSaas) {
return {
id: companyInfoFromCore.id,
name: companyInfoFromCore.name,
};
}
const isUserAdmin = userRole === UserRoleEnum.ADMIN;
Expand Down
7 changes: 4 additions & 3 deletions backend/src/exceptions/text/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const Messages = {
COMPANY_ALREADY_EXISTS: 'Company already exists',
COMPANY_NOT_EXISTS_IN_CONNECTION: `Connection does not attached to company. Please contact our support team`,
COMPANY_NOT_FOUND: 'Company not found. Please contact our support team',
COMPANY_NAME_UPDATE_FAILED_UNHANDLED_ERROR: `Failed to update company name. Please contact our support team.`,
COMPANIES_USER_EMAIL_NOT_FOUND: (email: string) => `No companies found for user ${email}`,
CONNECTION_ID_MISSING: 'Connection id is missing',
CONNECTION_NOT_CREATED: 'Connection was not successfully created.',
Expand Down Expand Up @@ -223,7 +224,7 @@ export const Messages = {
USER_ALREADY_ADDED_BUT_NOT_ACTIVE:
'User already added in this group, but email is not confirmed. We sent new invitation on this users email.',
USER_ALREADY_ADDED_BUT_NOT_ACTIVE_IN_COMPANY:
'User already added in this company, but email is not confirmed. We sent new invitation on this users email.',
'User already added in this company, but email is not confirmed. We sent new invitation on this users email.',
USER_CREATE_CONNECTION: (email: string, connectionType: any) =>
`Connection of type "${connectionType}" was added by user "${email}".`,
USER_CREATED: (email: string, provider: ProviderTypeEnum = null) =>
Expand Down Expand Up @@ -290,9 +291,9 @@ export const Messages = {
MAXIMUM_FREE_INVITATION_REACHED: 'Sorry, reached maximum number of users for free plan',
MAXIMUM_FREE_INVITATION_REACHED_CANNOT_BE_INVITED:
'Sorry you can not join this group because reached maximum number of users for free plan. Please ask you connection owner to upgrade plan or delete unnecessary user from group',
MAXIMUM_FREE_INVITATION_REACHED_CANNOT_BE_INVITED_IN_COMPANY:
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 unnecessary user from company',
FAILED_CREATE_SUBSCRIPTION_LOG: 'Failed to create subscription log. Please contact our support team.',
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`,
FAILED_REMOVE_USER_SAAS_UNHANDLED_ERROR: `Failed to remove user from company. Please contact our support team.`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ export class SaasCompanyGatewayService extends BaseSaasGatewayService {
return null;
}

public async updateCompanyName(companyId: string, newCompanyName: string): Promise<SuccessResponse | null> {
const result = await this.sendRequestToSaaS(`/webhook/company/update/name`, 'POST', {
companyId,
newCompanyName,
});
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 @@ -6,4 +6,7 @@ export class RegisterCompanyWebhookDS {

@ApiProperty()
registrarUserId: string;

@ApiProperty()
companyName: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ export class SaasController {
async companyRegistered(
@Body('userId') registrarUserId: string,
@Body('companyId') companyId: string,
@Body('companyName') companyName: string,
): Promise<RegisteredCompanyDS> {
const result = await this.companyRegistrationUseCase.execute({ companyId, registrarUserId });
const result = await this.companyRegistrationUseCase.execute({ companyId, registrarUserId, companyName });
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class RegisteredCompanyWebhookUseCase
}

protected async implementation(inputData: RegisterCompanyWebhookDS): Promise<RegisteredCompanyDS> {
const { companyId, registrarUserId } = inputData;
const { companyId, registrarUserId, companyName } = inputData;
const foundUser = await this._dbContext.userRepository.findOneUserById(registrarUserId);
if (!foundUser) {
throw new HttpException(
Expand All @@ -41,6 +41,7 @@ export class RegisteredCompanyWebhookUseCase
);
}
const newCompanyInfo = new CompanyInfoEntity();
newCompanyInfo.name = companyName;
newCompanyInfo.id = companyId;
newCompanyInfo.users = [foundUser];
const savedCompanyInfo = await this._dbContext.companyInfoRepository.save(newCompanyInfo);
Expand Down
17 changes: 17 additions & 0 deletions backend/src/migrations/1700575487164-AddNameInCompanyInfoEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddNameInCompanyInfoEntity1700575487164 implements MigrationInterface {
name = 'AddNameInCompanyInfoEntity1700575487164';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "company_info" ADD "name" character varying(255)`);
await queryRunner.query(
`ALTER TABLE "company_info" ADD CONSTRAINT "UQ_a46f1a801acc21386a5fce0d420" UNIQUE ("name")`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "company_info" DROP CONSTRAINT "UQ_a46f1a801acc21386a5fce0d420"`);
await queryRunner.query(`ALTER TABLE "company_info" DROP COLUMN "name"`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { AllExceptionsFilter } from '../../../src/exceptions/all-exceptions.filt
import { createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions } from '../../utils/user-with-different-permissions-utils.js';
import { ValidationException } from '../../../src/exceptions/custom-exceptions/validation-exception.js';
import { ValidationError } from 'class-validator';
import { faker } from '@faker-js/faker';
import { nanoid } from 'nanoid';

const mockFactory = new MockFactory();
let app: INestApplication;
Expand Down Expand Up @@ -66,7 +68,8 @@ test(`${currentTest} should return found company info for user`, async (t) => {
t.is(foundCompanyInfo.status, 200);
const foundCompanyInfoRO = JSON.parse(foundCompanyInfo.text);
t.is(foundCompanyInfoRO.hasOwnProperty('id'), true);
t.is(Object.keys(foundCompanyInfoRO).length, 1);
t.is(foundCompanyInfoRO.hasOwnProperty('name'), true);
t.is(Object.keys(foundCompanyInfoRO).length, 2);
} catch (error) {
console.error(error);
}
Expand Down Expand Up @@ -102,7 +105,8 @@ test(`${currentTest} should return full found company info for company admin use

t.is(foundCompanyInfo.status, 200);
t.is(foundCompanyInfoRO.hasOwnProperty('id'), true);
t.is(Object.keys(foundCompanyInfoRO).length, 3);
t.is(foundCompanyInfoRO.hasOwnProperty('name'), true);
t.is(Object.keys(foundCompanyInfoRO).length, 4);
t.is(foundCompanyInfoRO.hasOwnProperty('connections'), true);
t.is(foundCompanyInfoRO.connections.length > 3, true);
t.is(foundCompanyInfoRO.hasOwnProperty('invitations'), true);
Expand Down Expand Up @@ -155,7 +159,8 @@ test(`${currentTest} should return found company info for non-admin user`, async

t.is(foundCompanyInfo.status, 200);
t.is(foundCompanyInfoRO.hasOwnProperty('id'), true);
t.is(Object.keys(foundCompanyInfoRO).length, 1);
t.is(foundCompanyInfoRO.hasOwnProperty('name'), true);
t.is(Object.keys(foundCompanyInfoRO).length, 2);
} catch (error) {
console.error(error);
throw error;
Expand Down Expand Up @@ -371,3 +376,49 @@ test(`${currentTest} should remove user invitation from company`, async (t) => {
}
});

currentTest = 'PUT company/name/:slug';

test(`${currentTest} should update company name`, 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 foundCompanyInfoRO = JSON.parse(foundCompanyInfo.text);
t.is(foundCompanyInfoRO.hasOwnProperty('name'), true);

const newName = `${faker.company.name()}_${nanoid(5)}`;

const updateCompanyNameResult = await request(app.getHttpServer())
.put(`/company/name/${foundCompanyInfoRO.id}`)
.send({
name: newName,
})
.set('Content-Type', 'application/json')
.set('Cookie', adminUserToken)
.set('Accept', 'application/json');
t.is(updateCompanyNameResult.status, 200);

const foundCompanyInfoAfterUpdate = 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 foundCompanyInfoROAfterUpdate = JSON.parse(foundCompanyInfoAfterUpdate.text);
t.is(foundCompanyInfoROAfterUpdate.hasOwnProperty('name'), true);
t.is(foundCompanyInfoROAfterUpdate.name, newName);
});
Loading