diff --git a/backend/src/common/data-injection.tokens.ts b/backend/src/common/data-injection.tokens.ts index b2d3cc3df..aa88d7c4b 100644 --- a/backend/src/common/data-injection.tokens.ts +++ b/backend/src/common/data-injection.tokens.ts @@ -43,6 +43,7 @@ export enum UseCaseType { REFRESH_CONNECTION_AGENT_TOKEN = 'REFRESH_CONNECTION_AGENT_TOKEN', VALIDATE_CONNECTION_MASTER_PASSWORD = 'VALIDATE_CONNECTION_MASTER_PASSWORD', UNFREEZE_CONNECTION = 'UNFREEZE_CONNECTION', + UPDATE_CONNECTION_TITLE = 'UPDATE_CONNECTION_TITLE', FIND_ALL_USER_GROUPS = 'FIND_ALL_USER_GROUPS', INVITE_USER_IN_GROUP = 'INVITE_USER_IN_GROUP', diff --git a/backend/src/entities/connection/application/data-structures/update-connection-title.ds.ts b/backend/src/entities/connection/application/data-structures/update-connection-title.ds.ts new file mode 100644 index 000000000..9b95b6fff --- /dev/null +++ b/backend/src/entities/connection/application/data-structures/update-connection-title.ds.ts @@ -0,0 +1,5 @@ +export class UpdateConnectionTitleDs { + connectionId: string; + userId: string; + title: string; +} diff --git a/backend/src/entities/connection/application/dto/update-connection-title.dto.ts b/backend/src/entities/connection/application/dto/update-connection-title.dto.ts new file mode 100644 index 000000000..75cf090d4 --- /dev/null +++ b/backend/src/entities/connection/application/dto/update-connection-title.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateConnectionTitleDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + title: string; +} diff --git a/backend/src/entities/connection/connection.controller.ts b/backend/src/entities/connection/connection.controller.ts index fe6ce9a29..dd5b5597b 100644 --- a/backend/src/entities/connection/connection.controller.ts +++ b/backend/src/entities/connection/connection.controller.ts @@ -41,6 +41,7 @@ import { GetGroupsInConnectionDs } from './application/data-structures/get-group import { GetPermissionsInConnectionDs } from './application/data-structures/get-permissions-in-connection.ds.js'; import { RestoredConnectionDs } from './application/data-structures/restored-connection.ds.js'; import { UpdateConnectionDs } from './application/data-structures/update-connection.ds.js'; +import { UpdateConnectionTitleDs } from './application/data-structures/update-connection-title.ds.js'; import { UpdateMasterPasswordDs } from './application/data-structures/update-master-password.ds.js'; import { ValidateConnectionMasterPasswordDs } from './application/data-structures/validate-connection-master-password.ds.js'; import { CreateConnectionDto } from './application/dto/create-connection.dto.js'; @@ -51,6 +52,7 @@ import { DeleteGroupFromConnectionDTO } from './application/dto/delete-group-fro import { FoundUserGroupsInConnectionDTO } from './application/dto/found-user-groups-in-connection.dto.js'; import { ConnectionTokenResponseDTO } from './application/dto/new-connection-token-response.dto.js'; import { TestConnectionResponseDTO } from './application/dto/test-connection-response.dto.js'; +import { UpdateConnectionTitleDto } from './application/dto/update-connection-title.dto.js'; import { UpdateMasterPasswordRequestBodyDto } from './application/dto/update-master-password-request-body.dto.js'; import { UpdatedConnectionResponseDTO } from './application/dto/updated-connection-response.dto.js'; import { ValidationResultRo } from './application/dto/validation-result.ro.js'; @@ -69,6 +71,7 @@ import { ITestConnection, IUnfreezeConnection, IUpdateConnection, + IUpdateConnectionTitle, IUpdateMasterPassword, IValidateConnectionMasterPassword, IValidateConnectionToken, @@ -120,6 +123,8 @@ export class ConnectionController { private readonly validateConnectionMasterPasswordUseCase: IValidateConnectionMasterPassword, @Inject(UseCaseType.UNFREEZE_CONNECTION) private readonly unfreezeConnectionUseCase: IUnfreezeConnection, + @Inject(UseCaseType.UPDATE_CONNECTION_TITLE) + private readonly updateConnectionTitleUseCase: IUpdateConnectionTitle, @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, private readonly amplitudeService: AmplitudeService, @@ -685,4 +690,29 @@ export class ConnectionController { } return await this.unfreezeConnectionUseCase.execute({ connectionId, userId }, InTransactionEnum.ON); } + + @ApiOperation({ summary: 'Update connection title' }) + @ApiBody({ type: UpdateConnectionTitleDto }) + @ApiResponse({ + status: 200, + type: SuccessResponse, + description: 'Connection title was updated.', + }) + @UseGuards(ConnectionEditGuard) + @Put('/connection/title/:connectionId') + async updateConnectionTitle( + @Body() titleData: UpdateConnectionTitleDto, + @SlugUuid('connectionId') connectionId: string, + @UserId() userId: string, + ): Promise { + if (!connectionId) { + throw new BadRequestException(Messages.CONNECTION_ID_MISSING); + } + const inputData: UpdateConnectionTitleDs = { + connectionId, + userId, + title: titleData.title, + }; + return await this.updateConnectionTitleUseCase.execute(inputData, InTransactionEnum.ON); + } } diff --git a/backend/src/entities/connection/connection.module.ts b/backend/src/entities/connection/connection.module.ts index 75510367a..807f42d78 100644 --- a/backend/src/entities/connection/connection.module.ts +++ b/backend/src/entities/connection/connection.module.ts @@ -31,6 +31,7 @@ import { RefreshConnectionAgentTokenUseCase } from './use-cases/refresh-connecti import { RestoreConnectionUseCase } from './use-cases/restore-connection-use.case.js'; import { TestConnectionUseCase } from './use-cases/test-connection.use.case.js'; import { UnfreezeConnectionUseCase } from './use-cases/unfreeze-connection.use.case.js'; +import { UpdateConnectionTitleUseCase } from './use-cases/update-connection-title.use.case.js'; import { UpdateConnectionUseCase } from './use-cases/update-connection.use.case.js'; import { UpdateConnectionMasterPasswordUseCase } from './use-cases/update-connection-master-password.use.case.js'; import { ValidateConnectionMasterPasswordUseCase } from './use-cases/validate-connection-master-password.use.case.js'; @@ -130,6 +131,10 @@ import { ValidateConnectionTokenUseCase } from './use-cases/validate-connection- provide: UseCaseType.UNFREEZE_CONNECTION, useClass: UnfreezeConnectionUseCase, }, + { + provide: UseCaseType.UPDATE_CONNECTION_TITLE, + useClass: UpdateConnectionTitleUseCase, + }, ], controllers: [ConnectionController], }) @@ -156,6 +161,7 @@ export class ConnectionModule implements NestModule { { path: '/connection/token/refresh/:connectionId', method: RequestMethod.GET }, { path: '/connection/masterpwd/verify/:connectionId', method: RequestMethod.GET }, { path: '/connection/unfreeze/:connectionId', method: RequestMethod.PUT }, + { path: '/connection/title/:connectionId', method: RequestMethod.PUT }, ) .apply(AuthWithApiMiddleware) .forRoutes({ path: 'connections', method: RequestMethod.GET }); diff --git a/backend/src/entities/connection/use-cases/update-connection-title.use.case.ts b/backend/src/entities/connection/use-cases/update-connection-title.use.case.ts new file mode 100644 index 000000000..3c31e14ae --- /dev/null +++ b/backend/src/entities/connection/use-cases/update-connection-title.use.case.ts @@ -0,0 +1,29 @@ +import { Inject, Injectable, 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 { SuccessResponse } from '../../../microservices/saas-microservice/data-structures/common-responce.ds.js'; +import { UpdateConnectionTitleDs } from '../application/data-structures/update-connection-title.ds.js'; +import { IUpdateConnectionTitle } from './use-cases.interfaces.js'; + +@Injectable({ scope: Scope.REQUEST }) +export class UpdateConnectionTitleUseCase + extends AbstractUseCase + implements IUpdateConnectionTitle +{ + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + ) { + super(); + } + + protected async implementation(inputData: UpdateConnectionTitleDs): Promise { + const { connectionId, title } = inputData; + + const connection = await this._dbContext.connectionRepository.findOne({ where: { id: connectionId } }); + connection.title = title; + await this._dbContext.connectionRepository.save(connection); + return { success: true }; + } +} diff --git a/backend/src/entities/connection/use-cases/use-cases.interfaces.ts b/backend/src/entities/connection/use-cases/use-cases.interfaces.ts index 0088bec6f..e50bd7dec 100644 --- a/backend/src/entities/connection/use-cases/use-cases.interfaces.ts +++ b/backend/src/entities/connection/use-cases/use-cases.interfaces.ts @@ -18,6 +18,7 @@ import { TestConnectionResultDs } from '../application/data-structures/test-conn import { TokenDs } from '../application/data-structures/token.ds.js'; import { UnfreezeConnectionDs } from '../application/data-structures/unfreeze-connection.ds.js'; import { UpdateConnectionDs } from '../application/data-structures/update-connection.ds.js'; +import { UpdateConnectionTitleDs } from '../application/data-structures/update-connection-title.ds.js'; import { UpdateMasterPasswordDs } from '../application/data-structures/update-master-password.ds.js'; import { ValidateConnectionMasterPasswordDs } from '../application/data-structures/validate-connection-master-password.ds.js'; import { CreatedConnectionDTO } from '../application/dto/created-connection.dto.js'; @@ -101,3 +102,7 @@ export interface IValidateConnectionMasterPassword { export interface IUnfreezeConnection { execute(inputData: UnfreezeConnectionDs, inTransaction: InTransactionEnum): Promise; } + +export interface IUpdateConnectionTitle { + execute(inputData: UpdateConnectionTitleDs, inTransaction: InTransactionEnum): Promise; +} diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-connection-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-connection-e2e.test.ts index 638c3df30..6582f45c7 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-connection-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-connection-e2e.test.ts @@ -1517,3 +1517,136 @@ currentTest = 'GET /connection/user/permissions'; // throw e; // } // }); + +currentTest = 'PUT /connection/title'; +test.serial(`${currentTest} should return success when updating connection title`, async (t) => { + const { newConnection } = getTestData(); + const { token } = await registerUserAndReturnUserInfo(app); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(newConnection) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + const createConnectionRO = JSON.parse(createConnectionResponse.text); + + const newTitle = 'New Connection Title'; + const updateTitleResponse = await request(app.getHttpServer()) + .put(`/connection/title/${createConnectionRO.id}`) + .send({ title: newTitle }) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(updateTitleResponse.status, 200); + const result = updateTitleResponse.body; + t.is(result.success, true); + + const getConnectionResponse = await request(app.getHttpServer()) + .get(`/connection/one/${createConnectionRO.id}`) + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getConnectionResponse.status, 200); + const connectionResult = JSON.parse(getConnectionResponse.text); + t.is(connectionResult.connection.title, newTitle); + + t.pass(); +}); + +test.serial(`${currentTest} should throw error when title is empty`, async (t) => { + const { newConnection } = getTestData(); + const { token } = await registerUserAndReturnUserInfo(app); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(newConnection) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + const createConnectionRO = JSON.parse(createConnectionResponse.text); + + const updateTitleResponse = await request(app.getHttpServer()) + .put(`/connection/title/${createConnectionRO.id}`) + .send({ title: '' }) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(updateTitleResponse.status, 400); + + t.pass(); +}); + +test.serial(`${currentTest} should throw error when title is not provided`, async (t) => { + const { newConnection } = getTestData(); + const { token } = await registerUserAndReturnUserInfo(app); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(newConnection) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + const createConnectionRO = JSON.parse(createConnectionResponse.text); + + const updateTitleResponse = await request(app.getHttpServer()) + .put(`/connection/title/${createConnectionRO.id}`) + .send({}) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(updateTitleResponse.status, 400); + + t.pass(); +}); + +test.serial( + `${currentTest} should update title of encrypted connection and connection should still work`, + async (t) => { + const { newConnection } = getTestData(); + const { token } = await registerUserAndReturnUserInfo(app); + newConnection.masterEncryption = true; + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(newConnection) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(createConnectionResponse.status, 201); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + + const newTitle = 'Renamed Encrypted Connection'; + const updateTitleResponse = await request(app.getHttpServer()) + .put(`/connection/title/${createConnectionRO.id}`) + .send({ title: newTitle }) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(updateTitleResponse.status, 200); + t.is(updateTitleResponse.body.success, true); + + const findOneResponse = await request(app.getHttpServer()) + .get(`/connection/one/${createConnectionRO.id}`) + .set('Content-Type', 'application/json') + .set('masterpwd', 'ahalaimahalai') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(findOneResponse.status, 200); + const connectionResult = findOneResponse.body.connection; + t.is(connectionResult.title, newTitle); + + t.pass(); + }, +); diff --git a/backend/test/ava-tests/saas-tests/connection-e2e.test.ts b/backend/test/ava-tests/saas-tests/connection-e2e.test.ts index 2c30a74b1..9a90514bb 100644 --- a/backend/test/ava-tests/saas-tests/connection-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/connection-e2e.test.ts @@ -1822,3 +1822,141 @@ test.serial(`${currentTest} should unfreeze connection`, async (t) => { const result = findOneResponce.body.connection; t.is(result.isFrozen, false); }); + +currentTest = 'PUT /connection/title'; +test.serial(`${currentTest} should return success when updating connection title`, async (t) => { + const { token } = await registerUserAndReturnUserInfo(app); + const newPgConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); + + const createConnectionResult = await request(app.getHttpServer()) + .post('/connection') + .send(newPgConnection) + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createConnectionResult.status, 201); + + const createConnectionRO = JSON.parse(createConnectionResult.text); + const { id } = createConnectionRO; + + const newTitle = 'Renamed Connection'; + const updateTitleResponse = await request(app.getHttpServer()) + .put(`/connection/title/${id}`) + .send({ title: newTitle }) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(updateTitleResponse.status, 200); + t.is(updateTitleResponse.body.success, true); + + const findOneResponse = await request(app.getHttpServer()) + .get(`/connection/one/${id}`) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(findOneResponse.status, 200); + t.is(findOneResponse.body.connection.title, newTitle); +}); + +test.serial(`${currentTest} should throw error when title is empty`, async (t) => { + const { token } = await registerUserAndReturnUserInfo(app); + const newPgConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); + + const createConnectionResult = await request(app.getHttpServer()) + .post('/connection') + .send(newPgConnection) + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createConnectionResult.status, 201); + + const createConnectionRO = JSON.parse(createConnectionResult.text); + + const updateTitleResponse = await request(app.getHttpServer()) + .put(`/connection/title/${createConnectionRO.id}`) + .send({ title: '' }) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(updateTitleResponse.status, 400); +}); + +test.serial(`${currentTest} should throw error when title is not provided`, async (t) => { + const { token } = await registerUserAndReturnUserInfo(app); + const newPgConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); + + const createConnectionResult = await request(app.getHttpServer()) + .post('/connection') + .send(newPgConnection) + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createConnectionResult.status, 201); + + const createConnectionRO = JSON.parse(createConnectionResult.text); + + const updateTitleResponse = await request(app.getHttpServer()) + .put(`/connection/title/${createConnectionRO.id}`) + .send({}) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(updateTitleResponse.status, 400); +}); + +test.serial( + `${currentTest} should update title of encrypted connection and connection should still work`, + async (t) => { + const { token } = await registerUserAndReturnUserInfo(app); + const newPgConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); + newPgConnection.masterEncryption = true; + + const createConnectionResult = await request(app.getHttpServer()) + .post('/connection') + .send(newPgConnection) + .set('masterpwd', 'ahalaimahalai') + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createConnectionResult.status, 201); + + const createConnectionRO = JSON.parse(createConnectionResult.text); + const { id } = createConnectionRO; + + const newTitle = 'Renamed Encrypted Connection'; + const updateTitleResponse = await request(app.getHttpServer()) + .put(`/connection/title/${id}`) + .send({ title: newTitle }) + .set('Content-Type', 'application/json') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(updateTitleResponse.status, 200); + t.is(updateTitleResponse.body.success, true); + + const findOneResponse = await request(app.getHttpServer()) + .get(`/connection/one/${id}`) + .set('Content-Type', 'application/json') + .set('masterpwd', 'ahalaimahalai') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(findOneResponse.status, 200); + t.is(findOneResponse.body.connection.title, newTitle); + + const findTablesResponse = await request(app.getHttpServer()) + .get(`/connection/tables/${id}`) + .set('Content-Type', 'application/json') + .set('masterpwd', 'ahalaimahalai') + .set('Cookie', token) + .set('Accept', 'application/json'); + + t.is(findTablesResponse.status, 200); + const tables = JSON.parse(findTablesResponse.text); + t.is(tables.length > 0, true); + }, +);