diff --git a/backend/src/common/data-injection.tokens.ts b/backend/src/common/data-injection.tokens.ts index 239e786ac..68acf0f62 100644 --- a/backend/src/common/data-injection.tokens.ts +++ b/backend/src/common/data-injection.tokens.ts @@ -114,6 +114,8 @@ export enum UseCaseType { FREEZE_CONNECTIONS_IN_COMPANY = 'FREEZE_CONNECTIONS_IN_COMPANY', UNFREEZE_CONNECTIONS_IN_COMPANY = 'UNFREEZE_CONNECTIONS_IN_COMPANY', SAAS_REGISTER_USER_WITH_SAML = 'SAAS_REGISTER_USER_WITH_SAML', + SAAS_CREATE_CONNECTION_FOR_HOSTED_DB = 'SAAS_CREATE_CONNECTION_FOR_HOSTED_DB', + SAAS_DELETE_CONNECTION_FOR_HOSTED_DB = 'SAAS_DELETE_CONNECTION_FOR_HOSTED_DB', INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP', VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP', diff --git a/backend/src/microservices/saas-microservice/data-structures/create-connecttion-for-selfhosted-db.dto.ts b/backend/src/microservices/saas-microservice/data-structures/create-connecttion-for-selfhosted-db.dto.ts new file mode 100644 index 000000000..e4492e18f --- /dev/null +++ b/backend/src/microservices/saas-microservice/data-structures/create-connecttion-for-selfhosted-db.dto.ts @@ -0,0 +1,64 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsString, IsUUID, Max, Min } from 'class-validator'; + +export class CreateConnectionForHostedDbDto { + @ApiProperty({ + description: 'Company ID', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsString() + @IsNotEmpty() + @IsUUID() + companyId: string; + + @ApiProperty({ + description: 'User ID', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsString() + @IsNotEmpty() + @IsUUID() + userId: string; + + @ApiProperty({ + description: 'Database name', + example: 'my_database', + }) + @IsString() + @IsNotEmpty() + databaseName: string; + + @ApiProperty({ + description: 'Database hostname', + example: 'localhost', + }) + @IsString() + @IsNotEmpty() + hostname: string; + + @ApiProperty({ + description: 'Database port', + example: 5432, + }) + @IsNotEmpty() + @IsNumber() + @Max(65535) + @Min(1) + port: number; + + @ApiProperty({ + description: 'Database username', + example: 'db_user', + }) + @IsString() + @IsNotEmpty() + username: string; + + @ApiProperty({ + description: 'Database password', + example: 'secure_password', + }) + @IsString() + @IsNotEmpty() + password: string; +} diff --git a/backend/src/microservices/saas-microservice/data-structures/delete-connection-for-hosted-db.dto.ts b/backend/src/microservices/saas-microservice/data-structures/delete-connection-for-hosted-db.dto.ts new file mode 100644 index 000000000..9dd1d37e7 --- /dev/null +++ b/backend/src/microservices/saas-microservice/data-structures/delete-connection-for-hosted-db.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; + +export class DeleteConnectionForHostedDbDto { + @ApiProperty({ + description: 'Company ID', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsNotEmpty() + @IsString() + @IsUUID() + companyId: string; + + @ApiProperty({ + description: 'Hosted db entity ID', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsNotEmpty() + @IsString() + @IsUUID() + hostedDatabaseId: string; + + @ApiProperty({ + description: 'Database name', + example: 'my_database', + }) + databaseName: string; +} diff --git a/backend/src/microservices/saas-microservice/saas.controller.ts b/backend/src/microservices/saas-microservice/saas.controller.ts index 22f4ef1bc..2fb59336b 100644 --- a/backend/src/microservices/saas-microservice/saas.controller.ts +++ b/backend/src/microservices/saas-microservice/saas.controller.ts @@ -31,6 +31,8 @@ import { SaasSAMLUserRegisterDS } from './data-structures/saas-saml-user-registe import { SaasRegisterUserWithGoogleDS } from './data-structures/sass-register-user-with-google.js'; import { ICompanyRegistration, + ICreateConnectionForHostedDb, + IDeleteConnectionForHostedDb, IFreezeConnectionsInCompany, IGetUserInfo, ILoginUserWithGitHub, @@ -44,6 +46,9 @@ import { ISuspendUsers, ISuspendUsersOverLimit, } from './use-cases/saas-use-cases.interface.js'; +import { CreatedConnectionDTO } from '../../entities/connection/application/dto/created-connection.dto.js'; +import { CreateConnectionForHostedDbDto } from './data-structures/create-connecttion-for-selfhosted-db.dto.js'; +import { DeleteConnectionForHostedDbDto } from './data-structures/delete-connection-for-hosted-db.dto.js'; @UseInterceptors(SentryInterceptor) @SkipThrottle() @@ -82,6 +87,10 @@ export class SaasController { private readonly freezeConnectionsInCompanyUseCase: IFreezeConnectionsInCompany, @Inject(UseCaseType.UNFREEZE_CONNECTIONS_IN_COMPANY) private readonly unfreezeConnectionsInCompanyUseCase: IFreezeConnectionsInCompany, + @Inject(UseCaseType.SAAS_CREATE_CONNECTION_FOR_HOSTED_DB) + private readonly createConnectionForHostedDbUseCase: ICreateConnectionForHostedDb, + @Inject(UseCaseType.SAAS_DELETE_CONNECTION_FOR_HOSTED_DB) + private readonly deleteConnectionForHostedDbUseCase: IDeleteConnectionForHostedDb, ) {} @ApiOperation({ summary: 'Company registered webhook' }) @@ -274,4 +283,30 @@ export class SaasController { samlAttributes, }); } + + @ApiOperation({ summary: 'Created connection of hosted database' }) + @ApiBody({ type: CreateConnectionForHostedDbDto }) + @ApiResponse({ + status: 201, + type: CreatedConnectionDTO, + }) + @Post('/connection/hosted') + async createConnectionForHostedDb( + @Body() connectionData: CreateConnectionForHostedDbDto, + ): Promise { + return await this.createConnectionForHostedDbUseCase.execute(connectionData); + } + + @ApiOperation({ summary: 'Delete connection of hosted database' }) + @ApiBody({ type: DeleteConnectionForHostedDbDto }) + @ApiResponse({ + status: 201, + type: CreatedConnectionDTO, + }) + @Post('connection/hosted/delete') + async deleteConnectionForHostedDb( + @Body() deleteConnectionData: DeleteConnectionForHostedDbDto, + ): Promise { + return await this.deleteConnectionForHostedDbUseCase.execute(deleteConnectionData); + } } diff --git a/backend/src/microservices/saas-microservice/saas.module.ts b/backend/src/microservices/saas-microservice/saas.module.ts index 65f2a9ecb..3c47d5289 100644 --- a/backend/src/microservices/saas-microservice/saas.module.ts +++ b/backend/src/microservices/saas-microservice/saas.module.ts @@ -20,6 +20,8 @@ import { SaaSRegisterUserWIthSamlUseCase } from './use-cases/register-user-with- import { SaasUsualRegisterUseCase } from './use-cases/saas-usual-register-user.use.case.js'; import { SuspendUsersUseCase } from './use-cases/suspend-users.use.case.js'; import { SuspendUsersOverLimitUseCase } from './use-cases/suspend-users-over-limit.use.case.js'; +import { CreateConnectionForHostedDbUseCase } from './use-cases/create-connection-for-hosted-db.use.case.js'; +import { DeleteConnectionForHostedDbUseCase } from './use-cases/delete-connection-for-hosted-db.use.case.js'; import { UnFreezeConnectionsInCompanyUseCase } from './use-cases/unfreeze-connections-in-company-use.case.js'; @Module({ @@ -85,6 +87,14 @@ import { UnFreezeConnectionsInCompanyUseCase } from './use-cases/unfreeze-connec provide: UseCaseType.SAAS_SUSPEND_USERS_OVER_LIMIT, useClass: SuspendUsersOverLimitUseCase, }, + { + provide: UseCaseType.SAAS_CREATE_CONNECTION_FOR_HOSTED_DB, + useClass: CreateConnectionForHostedDbUseCase, + }, + { + provide: UseCaseType.SAAS_DELETE_CONNECTION_FOR_HOSTED_DB, + useClass: DeleteConnectionForHostedDbUseCase, + }, SignInAuditService, ], controllers: [SaasController], @@ -108,6 +118,8 @@ export class SaasModule { { path: 'saas/company/freeze-connections', method: RequestMethod.PUT }, { path: 'saas/company/unfreeze-connections', method: RequestMethod.PUT }, { path: 'saas/user/saml/login', method: RequestMethod.POST }, + { path: 'saas/connection/hosted', method: RequestMethod.POST }, + { path: 'saas/connection/hosted/delete', method: RequestMethod.POST }, ); } } diff --git a/backend/src/microservices/saas-microservice/use-cases/create-connection-for-hosted-db.use.case.ts b/backend/src/microservices/saas-microservice/use-cases/create-connection-for-hosted-db.use.case.ts new file mode 100644 index 000000000..202bacf20 --- /dev/null +++ b/backend/src/microservices/saas-microservice/use-cases/create-connection-for-hosted-db.use.case.ts @@ -0,0 +1,119 @@ +import { Inject, Injectable, InternalServerErrorException, Scope } from '@nestjs/common'; +import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js'; +import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; +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 { Messages } from '../../../exceptions/text/messages.js'; +import { slackPostMessage } from '../../../helpers/index.js'; +import { AccessLevelEnum } from '../../../enums/index.js'; +import { generateCedarPolicyForGroup } from '../../../entities/cedar-authorization/cedar-policy-generator.js'; +import { CreatedConnectionDTO } from '../../../entities/connection/application/dto/created-connection.dto.js'; +import { ConnectionEntity } from '../../../entities/connection/connection.entity.js'; +import { readSslCertificate } from '../../../entities/connection/ssl-certificate/read-certificate.js'; +import { buildCreatedConnectionDs } from '../../../entities/connection/utils/build-created-connection.ds.js'; +import { CreateConnectionForHostedDbDto } from '../data-structures/create-connecttion-for-selfhosted-db.dto.js'; +import { ICreateConnectionForHostedDb } from './saas-use-cases.interface.js'; + +@Injectable({ scope: Scope.REQUEST }) +export class CreateConnectionForHostedDbUseCase + extends AbstractUseCase + implements ICreateConnectionForHostedDb +{ + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + ) { + super(); + } + + protected async implementation(inputData: CreateConnectionForHostedDbDto): Promise { + const { companyId, userId, databaseName, hostname, port, username, password } = inputData; + + const connectionAuthor = await this._dbContext.userRepository.findOneUserById(userId); + if (!connectionAuthor) { + throw new InternalServerErrorException(Messages.USER_NOT_FOUND); + } + + await slackPostMessage( + Messages.USER_TRY_CREATE_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres), + ); + + const cert = await readSslCertificate(); + + const connectionParams = { + type: ConnectionTypesEnum.postgres, + host: hostname, + port: port, + username: username, + password: password, + database: databaseName, + schema: null, + sid: null, + ssh: false, + privateSSHKey: null, + sshHost: null, + sshPort: null, + sshUsername: null, + ssl: true, + cert: cert, + azure_encryption: false, + authSource: null, + dataCenter: null, + }; + + const dao = getDataAccessObject(connectionParams); + await dao.testConnect(); + + const connection = new ConnectionEntity(); + connection.type = ConnectionTypesEnum.postgres; + connection.host = hostname; + connection.port = port; + connection.username = username; + connection.password = password; + connection.database = databaseName; + connection.ssl = true; + connection.cert = cert; + connection.ssh = false; + connection.azure_encryption = false; + connection.masterEncryption = false; + connection.author = connectionAuthor; + + const savedConnection = await this._dbContext.connectionRepository.saveNewConnection(connection); + + const createdAdminGroup = await this._dbContext.groupRepository.createdAdminGroupInConnection( + savedConnection, + connectionAuthor, + ); + await this._dbContext.permissionRepository.createdDefaultAdminPermissionsInGroup(createdAdminGroup); + createdAdminGroup.cedarPolicy = generateCedarPolicyForGroup( + savedConnection.id, + true, + { + connection: { connectionId: savedConnection.id, accessLevel: AccessLevelEnum.edit }, + group: { groupId: createdAdminGroup.id, accessLevel: AccessLevelEnum.edit }, + tables: [], + }, + ); + await this._dbContext.groupRepository.saveNewOrUpdatedGroup(createdAdminGroup); + delete createdAdminGroup.connection; + await this._dbContext.userRepository.saveUserEntity(connectionAuthor); + savedConnection.groups = [createdAdminGroup]; + + const foundCompany = await this._dbContext.companyInfoRepository.findCompanyInfoByCompanyIdWithoutConnections(companyId); + if (foundCompany) { + const connectionToUpdate = await this._dbContext.connectionRepository.findOne({ + where: { id: savedConnection.id }, + }); + connectionToUpdate.company = foundCompany; + await this._dbContext.connectionRepository.saveUpdatedConnection(connectionToUpdate); + } + + await slackPostMessage( + Messages.USER_CREATED_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres), + ); + + const connectionRO = buildCreatedConnectionDs(savedConnection, null, null); + return connectionRO; + } +} diff --git a/backend/src/microservices/saas-microservice/use-cases/delete-connection-for-hosted-db.use.case.ts b/backend/src/microservices/saas-microservice/use-cases/delete-connection-for-hosted-db.use.case.ts new file mode 100644 index 000000000..84e2025c9 --- /dev/null +++ b/backend/src/microservices/saas-microservice/use-cases/delete-connection-for-hosted-db.use.case.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable, 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 { Messages } from '../../../exceptions/text/messages.js'; +import { CreatedConnectionDTO } from '../../../entities/connection/application/dto/created-connection.dto.js'; +import { buildCreatedConnectionDs } from '../../../entities/connection/utils/build-created-connection.ds.js'; +import { DeleteConnectionForHostedDbDto } from '../data-structures/delete-connection-for-hosted-db.dto.js'; +import { IDeleteConnectionForHostedDb } from './saas-use-cases.interface.js'; + +@Injectable({ scope: Scope.REQUEST }) +export class DeleteConnectionForHostedDbUseCase + extends AbstractUseCase + implements IDeleteConnectionForHostedDb +{ + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + ) { + super(); + } + + protected async implementation(inputData: DeleteConnectionForHostedDbDto): Promise { + const { companyId, hostedDatabaseId } = inputData; + + const connectionToDelete = await this._dbContext.connectionRepository.findAndDecryptConnection( + hostedDatabaseId, + null, + ); + if (!connectionToDelete) { + throw new NotFoundException(Messages.CONNECTION_NOT_FOUND); + } + + const foundCompany = await this._dbContext.companyInfoRepository.findCompanyInfoByCompanyIdWithoutConnections(companyId); + if (!foundCompany) { + throw new NotFoundException(Messages.COMPANY_NOT_FOUND); + } + + const result = await this._dbContext.connectionRepository.removeConnection(connectionToDelete); + return buildCreatedConnectionDs(result, null, null); + } +} diff --git a/backend/src/microservices/saas-microservice/use-cases/saas-use-cases.interface.ts b/backend/src/microservices/saas-microservice/use-cases/saas-use-cases.interface.ts index 438cadd9c..bbf223d9d 100644 --- a/backend/src/microservices/saas-microservice/use-cases/saas-use-cases.interface.ts +++ b/backend/src/microservices/saas-microservice/use-cases/saas-use-cases.interface.ts @@ -1,10 +1,13 @@ import { CompanyInfoEntity } from '../../../entities/company-info/company-info.entity.js'; +import { CreatedConnectionDTO } from '../../../entities/connection/application/dto/created-connection.dto.js'; import { SaaSRegisterDemoUserAccountDS } from '../../../entities/user/application/data-structures/demo-user-account-register.ds.js'; import { SaasUsualUserRegisterDS } from '../../../entities/user/application/data-structures/usual-register-user.ds.js'; import { FoundUserDto } from '../../../entities/user/dto/found-user.dto.js'; import { UserEntity } from '../../../entities/user/user.entity.js'; import { InTransactionEnum } from '../../../enums/in-transaction.enum.js'; import { SuccessResponse } from '../data-structures/common-responce.ds.js'; +import { CreateConnectionForHostedDbDto } from '../data-structures/create-connecttion-for-selfhosted-db.dto.js'; +import { DeleteConnectionForHostedDbDto } from '../data-structures/delete-connection-for-hosted-db.dto.js'; import { FreezeConnectionsInCompanyDS } from '../data-structures/freeze-connections-in-company.ds.js'; import { GetUserInfoByIdDS } from '../data-structures/get-user-info.ds.js'; import { GetUsersInfosByEmailDS } from '../data-structures/get-users-infos-by-email.ds.js'; @@ -66,3 +69,11 @@ export interface IFreezeConnectionsInCompany { export interface ISaasSAMLRegisterUser { execute(userData: SaasSAMLUserRegisterDS): Promise; } + +export interface ICreateConnectionForHostedDb { + execute(inputData: CreateConnectionForHostedDbDto): Promise; +} + +export interface IDeleteConnectionForHostedDb { + execute(inputData: DeleteConnectionForHostedDbDto): Promise; +}