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/src/common/data-injection.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class UpdateConnectionTitleDs {
connectionId: string;
userId: string;
title: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class UpdateConnectionTitleDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
title: string;
}
30 changes: 30 additions & 0 deletions backend/src/entities/connection/connection.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -69,6 +71,7 @@ import {
ITestConnection,
IUnfreezeConnection,
IUpdateConnection,
IUpdateConnectionTitle,
IUpdateMasterPassword,
IValidateConnectionMasterPassword,
IValidateConnectionToken,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<SuccessResponse> {
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);
}
}
6 changes: 6 additions & 0 deletions backend/src/entities/connection/connection.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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],
})
Expand All @@ -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 });
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UpdateConnectionTitleDs, SuccessResponse>
implements IUpdateConnectionTitle
{
constructor(
@Inject(BaseType.GLOBAL_DB_CONTEXT)
protected _dbContext: IGlobalDatabaseContext,
) {
super();
}

protected async implementation(inputData: UpdateConnectionTitleDs): Promise<SuccessResponse> {
const { connectionId, title } = inputData;

const connection = await this._dbContext.connectionRepository.findOne({ where: { id: connectionId } });
connection.title = title;
await this._dbContext.connectionRepository.save(connection);
Comment on lines +21 to +26

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findOne may return null when the connectionId doesn't exist; the next line will throw (attempting to set connection.title) and the endpoint will respond with a 500. Please handle the not-found case explicitly (e.g., throw NotFoundException(Messages.CONNECTION_NOT_FOUND) / BadRequestException, consistent with other connection use-cases) before mutating/saving.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +26

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation performs a read (findOne) followed by a full entity save() just to update title, which is an extra round-trip and can write more columns than necessary. Consider using a single update({ id: connectionId }, { title }) (or query builder) and check affected rows to confirm the connection exists.

Suggested change
const connection = await this._dbContext.connectionRepository.findOne({ where: { id: connectionId } });
connection.title = title;
await this._dbContext.connectionRepository.save(connection);
const updateResult = await this._dbContext.connectionRepository.update(
{ id: connectionId },
{ title },
);
if (!updateResult.affected) {
throw new Error(`Connection with id ${connectionId} not found`);
}

Copilot uses AI. Check for mistakes.
return { success: true };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -101,3 +102,7 @@ export interface IValidateConnectionMasterPassword {
export interface IUnfreezeConnection {
execute(inputData: UnfreezeConnectionDs, inTransaction: InTransactionEnum): Promise<SuccessResponse>;
}

export interface IUpdateConnectionTitle {
execute(inputData: UpdateConnectionTitleDs, inTransaction: InTransactionEnum): Promise<SuccessResponse>;
}
133 changes: 133 additions & 0 deletions backend/test/ava-tests/non-saas-tests/non-saas-connection-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},
);
Loading
Loading