diff --git a/backend/src/common/data-injection.tokens.ts b/backend/src/common/data-injection.tokens.ts index 68acf0f62..b2d3cc3df 100644 --- a/backend/src/common/data-injection.tokens.ts +++ b/backend/src/common/data-injection.tokens.ts @@ -116,6 +116,7 @@ export enum UseCaseType { 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', + SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD = 'SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD', 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/common-responce.ds.ts b/backend/src/microservices/saas-microservice/data-structures/common-responce.ds.ts index 88594c82d..4a51be5e7 100644 --- a/backend/src/microservices/saas-microservice/data-structures/common-responce.ds.ts +++ b/backend/src/microservices/saas-microservice/data-structures/common-responce.ds.ts @@ -4,3 +4,8 @@ export class SuccessResponse { @ApiProperty() success: boolean; } + +export class CreatedConnectionResponse { + @ApiProperty() + connectionId: string; +} diff --git a/backend/src/microservices/saas-microservice/data-structures/update-hosted-connection-password.dto.ts b/backend/src/microservices/saas-microservice/data-structures/update-hosted-connection-password.dto.ts new file mode 100644 index 000000000..5b3305b62 --- /dev/null +++ b/backend/src/microservices/saas-microservice/data-structures/update-hosted-connection-password.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; + +export class UpdateHostedConnectionPasswordDto { + @ApiProperty({ + description: 'Company ID', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsNotEmpty() + @IsString() + @IsUUID() + companyId: string; + + @ApiProperty({ + description: 'Database name', + example: 'my_database', + }) + @IsNotEmpty() + @IsString() + databaseName: string; + + @ApiProperty({ + description: 'New database password', + example: 'new_secure_password', + }) + @IsNotEmpty() + @IsString() + password: string; +} diff --git a/backend/src/microservices/saas-microservice/saas.controller.ts b/backend/src/microservices/saas-microservice/saas.controller.ts index 32263dd57..f9f876ff9 100644 --- a/backend/src/microservices/saas-microservice/saas.controller.ts +++ b/backend/src/microservices/saas-microservice/saas.controller.ts @@ -16,6 +16,7 @@ import { SkipThrottle } from '@nestjs/throttler'; import { UseCaseType } from '../../common/data-injection.tokens.js'; import { Timeout } from '../../decorators/timeout.decorator.js'; import { CompanyInfoEntity } from '../../entities/company-info/company-info.entity.js'; +import { CreatedConnectionDTO } from '../../entities/connection/application/dto/created-connection.dto.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 { ExternalRegistrationProviderEnum } from '../../entities/user/enums/external-registration-provider.enum.js'; @@ -23,12 +24,15 @@ import { UserEntity } from '../../entities/user/user.entity.js'; import { InTransactionEnum } from '../../enums/in-transaction.enum.js'; import { Messages } from '../../exceptions/text/messages.js'; import { SentryInterceptor } from '../../interceptors/sentry.interceptor.js'; -import { SuccessResponse } from './data-structures/common-responce.ds.js'; +import { CreatedConnectionResponse, 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 { RegisterCompanyWebhookDS } from './data-structures/register-company.ds.js'; import { RegisteredCompanyDS } from './data-structures/registered-company.ds.js'; import { SaasRegisterUserWithGithub } from './data-structures/saas-register-user-with-github.js'; import { SaasSAMLUserRegisterDS } from './data-structures/saas-saml-user-register.ds.js'; import { SaasRegisterUserWithGoogleDS } from './data-structures/sass-register-user-with-google.js'; +import { UpdateHostedConnectionPasswordDto } from './data-structures/update-hosted-connection-password.dto.js'; import { ICompanyRegistration, ICreateConnectionForHostedDb, @@ -45,10 +49,8 @@ import { ISaasSAMLRegisterUser, ISuspendUsers, ISuspendUsersOverLimit, + IUpdateHostedConnectionPassword, } 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() @@ -91,6 +93,8 @@ export class SaasController { private readonly createConnectionForHostedDbUseCase: ICreateConnectionForHostedDb, @Inject(UseCaseType.SAAS_DELETE_CONNECTION_FOR_HOSTED_DB) private readonly deleteConnectionForHostedDbUseCase: IDeleteConnectionForHostedDb, + @Inject(UseCaseType.SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD) + private readonly updateHostedConnectionPasswordUseCase: IUpdateHostedConnectionPassword, ) {} @ApiOperation({ summary: 'Company registered webhook' }) @@ -288,12 +292,12 @@ export class SaasController { @ApiBody({ type: CreateConnectionForHostedDbDto }) @ApiResponse({ status: 201, - type: CreatedConnectionDTO, + type: CreatedConnectionResponse, }) @Post('/connection/hosted') async createConnectionForHostedDb( @Body() connectionData: CreateConnectionForHostedDbDto, - ): Promise { + ): Promise { return await this.createConnectionForHostedDbUseCase.execute(connectionData); } @@ -309,4 +313,17 @@ export class SaasController { ): Promise { return await this.deleteConnectionForHostedDbUseCase.execute(deleteConnectionData); } + + @ApiOperation({ summary: 'Update password of hosted database connection' }) + @ApiBody({ type: UpdateHostedConnectionPasswordDto }) + @ApiResponse({ + status: 201, + type: SuccessResponse, + }) + @Post('/connection/hosted/password') + async updateHostedConnectionPassword( + @Body() updatePasswordData: UpdateHostedConnectionPasswordDto, + ): Promise { + return await this.updateHostedConnectionPasswordUseCase.execute(updatePasswordData); + } } diff --git a/backend/src/microservices/saas-microservice/saas.module.ts b/backend/src/microservices/saas-microservice/saas.module.ts index 3c47d5289..88b28bbb7 100644 --- a/backend/src/microservices/saas-microservice/saas.module.ts +++ b/backend/src/microservices/saas-microservice/saas.module.ts @@ -7,6 +7,8 @@ import { UserEntity } from '../../entities/user/user.entity.js'; import { SignInAuditEntity } from '../../entities/user-sign-in-audit/sign-in-audit.entity.js'; import { SignInAuditService } from '../../entities/user-sign-in-audit/sign-in-audit.service.js'; import { SaasController } from './saas.controller.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 { FreezeConnectionsInCompanyUseCase } from './use-cases/freeze-connections-in-company.use.case.js'; import { GetFullCompanyInfoByUserIdUseCase } from './use-cases/get-full-company-info-by-user-id.use.case.js'; import { GetUserInfoUseCase } from './use-cases/get-user-info.use.case.js'; @@ -20,9 +22,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'; +import { UpdateHostedConnectionPasswordUseCase } from './use-cases/update-hosted-connection-password.use.case.js'; @Module({ imports: [TypeOrmModule.forFeature([SignInAuditEntity, UserEntity])], @@ -95,6 +96,10 @@ import { UnFreezeConnectionsInCompanyUseCase } from './use-cases/unfreeze-connec provide: UseCaseType.SAAS_DELETE_CONNECTION_FOR_HOSTED_DB, useClass: DeleteConnectionForHostedDbUseCase, }, + { + provide: UseCaseType.SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD, + useClass: UpdateHostedConnectionPasswordUseCase, + }, SignInAuditService, ], controllers: [SaasController], @@ -120,6 +125,7 @@ export class SaasModule { { path: 'saas/user/saml/login', method: RequestMethod.POST }, { path: 'saas/connection/hosted', method: RequestMethod.POST }, { path: 'saas/connection/hosted/delete', method: RequestMethod.POST }, + { path: 'saas/connection/hosted/password', 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 index 517b1670d..04fdd1081 100644 --- 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 @@ -4,20 +4,19 @@ import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/en 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 { AccessLevelEnum } from '../../../enums/index.js'; +import { Messages } from '../../../exceptions/text/messages.js'; +import { slackPostMessage } from '../../../helpers/index.js'; +import { CreatedConnectionResponse } from '../data-structures/common-responce.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 + extends AbstractUseCase implements ICreateConnectionForHostedDb { constructor( @@ -27,7 +26,7 @@ export class CreateConnectionForHostedDbUseCase super(); } - protected async implementation(inputData: CreateConnectionForHostedDbDto): Promise { + protected async implementation(inputData: CreateConnectionForHostedDbDto): Promise { const { companyId, userId, databaseName, hostname, port, username, password } = inputData; const connectionAuthor = await this._dbContext.userRepository.findOneUserById(userId); @@ -35,9 +34,7 @@ export class CreateConnectionForHostedDbUseCase throw new InternalServerErrorException(Messages.USER_NOT_FOUND); } - await slackPostMessage( - Messages.USER_TRY_CREATE_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres), - ); + await slackPostMessage(Messages.USER_TRY_CREATE_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres)); const cert = await readSslCertificate(); @@ -85,21 +82,18 @@ export class CreateConnectionForHostedDbUseCase savedConnection, connectionAuthor, ); - createdAdminGroup.cedarPolicy = generateCedarPolicyForGroup( - savedConnection.id, - true, - { - connection: { connectionId: savedConnection.id, accessLevel: AccessLevelEnum.edit }, - group: { groupId: createdAdminGroup.id, accessLevel: AccessLevelEnum.edit }, - tables: [], - }, - ); + 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); + const foundCompany = + await this._dbContext.companyInfoRepository.findCompanyInfoByCompanyIdWithoutConnections(companyId); if (foundCompany) { const connectionToUpdate = await this._dbContext.connectionRepository.findOne({ where: { id: savedConnection.id }, @@ -108,11 +102,8 @@ export class CreateConnectionForHostedDbUseCase await this._dbContext.connectionRepository.saveUpdatedConnection(connectionToUpdate); } - await slackPostMessage( - Messages.USER_CREATED_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres), - ); + await slackPostMessage(Messages.USER_CREATED_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres)); - const connectionRO = buildCreatedConnectionDs(savedConnection, null, null); - return connectionRO; + return { connectionId: savedConnection.id }; } } 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 bbf223d9d..40b996e4e 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 @@ -5,7 +5,7 @@ import { SaasUsualUserRegisterDS } from '../../../entities/user/application/data 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 { CreatedConnectionResponse, 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'; @@ -17,6 +17,7 @@ import { SaasRegisterUserWithGithub } from '../data-structures/saas-register-use import { SaasSAMLUserRegisterDS } from '../data-structures/saas-saml-user-register.ds.js'; import { SaasRegisterUserWithGoogleDS } from '../data-structures/sass-register-user-with-google.js'; import { SuspendUsersDS } from '../data-structures/suspend-users.ds.js'; +import { UpdateHostedConnectionPasswordDto } from '../data-structures/update-hosted-connection-password.dto.js'; export interface ICompanyRegistration { execute(inputData: RegisterCompanyWebhookDS): Promise; @@ -71,9 +72,13 @@ export interface ISaasSAMLRegisterUser { } export interface ICreateConnectionForHostedDb { - execute(inputData: CreateConnectionForHostedDbDto): Promise; + execute(inputData: CreateConnectionForHostedDbDto): Promise; } export interface IDeleteConnectionForHostedDb { execute(inputData: DeleteConnectionForHostedDbDto): Promise; } + +export interface IUpdateHostedConnectionPassword { + execute(inputData: UpdateHostedConnectionPasswordDto): Promise; +} diff --git a/backend/src/microservices/saas-microservice/use-cases/update-hosted-connection-password.use.case.ts b/backend/src/microservices/saas-microservice/use-cases/update-hosted-connection-password.use.case.ts new file mode 100644 index 000000000..76beba9db --- /dev/null +++ b/backend/src/microservices/saas-microservice/use-cases/update-hosted-connection-password.use.case.ts @@ -0,0 +1,44 @@ +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 { SuccessResponse } from '../data-structures/common-responce.ds.js'; +import { UpdateHostedConnectionPasswordDto } from '../data-structures/update-hosted-connection-password.dto.js'; +import { IUpdateHostedConnectionPassword } from './saas-use-cases.interface.js'; + +@Injectable({ scope: Scope.REQUEST }) +export class UpdateHostedConnectionPasswordUseCase + extends AbstractUseCase + implements IUpdateHostedConnectionPassword +{ + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + ) { + super(); + } + + protected async implementation(inputData: UpdateHostedConnectionPasswordDto): Promise { + const { companyId, databaseName, password } = inputData; + + const foundCompany = + await this._dbContext.companyInfoRepository.findCompanyInfoByCompanyIdWithoutConnections(companyId); + if (!foundCompany) { + throw new NotFoundException(Messages.COMPANY_NOT_FOUND); + } + + const companyConnections = await this._dbContext.connectionRepository.find({ + where: { company: { id: companyId } }, + }); + const connection = companyConnections.find((conn) => conn.database === databaseName); + if (!connection) { + throw new NotFoundException(Messages.CONNECTION_NOT_FOUND); + } + + connection.password = password; + await this._dbContext.connectionRepository.saveUpdatedConnection(connection); + + return { success: true }; + } +}