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 @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ export class SuccessResponse {
@ApiProperty()
success: boolean;
}

export class CreatedConnectionResponse {
@ApiProperty()
connectionId: string;
}
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +14 to +20
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Using databaseName as the identifier for the hosted connection is ambiguous (a company can have multiple connections with the same DB name) and, because the database column is encrypted at rest, it can’t be used for direct DB lookups. Consider including a hostedDatabaseId/connectionId in this DTO (consistent with DeleteConnectionForHostedDbDto) and using that to target the exact connection.

Copilot uses AI. Check for mistakes.

@ApiProperty({
description: 'New database password',
example: 'new_secure_password',
})
@IsNotEmpty()
@IsString()
password: string;
}
29 changes: 23 additions & 6 deletions backend/src/microservices/saas-microservice/saas.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,23 @@ 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';
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,
Expand All @@ -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()
Expand Down Expand Up @@ -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' })
Expand Down Expand Up @@ -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<CreatedConnectionDTO> {
): Promise<CreatedConnectionResponse> {
return await this.createConnectionForHostedDbUseCase.execute(connectionData);
}

Expand All @@ -309,4 +313,17 @@ export class SaasController {
): Promise<CreatedConnectionDTO> {
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<SuccessResponse> {
return await this.updateHostedConnectionPasswordUseCase.execute(updatePasswordData);
}
}
10 changes: 8 additions & 2 deletions backend/src/microservices/saas-microservice/saas.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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])],
Expand Down Expand Up @@ -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],
Expand All @@ -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 },
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<CreateConnectionForHostedDbDto, CreatedConnectionDTO>
extends AbstractUseCase<CreateConnectionForHostedDbDto, CreatedConnectionResponse>
implements ICreateConnectionForHostedDb
{
constructor(
Expand All @@ -27,17 +26,15 @@ export class CreateConnectionForHostedDbUseCase
super();
}

protected async implementation(inputData: CreateConnectionForHostedDbDto): Promise<CreatedConnectionDTO> {
protected async implementation(inputData: CreateConnectionForHostedDbDto): Promise<CreatedConnectionResponse> {
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),
);
await slackPostMessage(Messages.USER_TRY_CREATE_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres));

const cert = await readSslCertificate();

Expand Down Expand Up @@ -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 },
Expand All @@ -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 };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<RegisteredCompanyDS>;
Expand Down Expand Up @@ -71,9 +72,13 @@ export interface ISaasSAMLRegisterUser {
}

export interface ICreateConnectionForHostedDb {
execute(inputData: CreateConnectionForHostedDbDto): Promise<CreatedConnectionDTO>;
execute(inputData: CreateConnectionForHostedDbDto): Promise<CreatedConnectionResponse>;
}

export interface IDeleteConnectionForHostedDb {
execute(inputData: DeleteConnectionForHostedDbDto): Promise<CreatedConnectionDTO>;
}

export interface IUpdateHostedConnectionPassword {
execute(inputData: UpdateHostedConnectionPasswordDto): Promise<SuccessResponse>;
}
Original file line number Diff line number Diff line change
@@ -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<UpdateHostedConnectionPasswordDto, SuccessResponse>
implements IUpdateHostedConnectionPassword
{
constructor(
@Inject(BaseType.GLOBAL_DB_CONTEXT)
protected _dbContext: IGlobalDatabaseContext,
) {
super();
}

protected async implementation(inputData: UpdateHostedConnectionPasswordDto): Promise<SuccessResponse> {
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);
}
Comment on lines +34 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a unique constraint on company+database
echo "=== Checking for unique constraints on connection entity ==="
rg -n "unique|@Unique|@Index" backend/src/entities/connection/connection.entity.ts -A 2 -B 2

echo "=== Checking how hosted connections are created ==="
rg -n "databaseName|database" backend/src/microservices/saas-microservice/use-cases/create-connection-for-hosted-db.use.case.ts

Repository: rocket-admin/rocketadmin

Length of output: 456


🏁 Script executed:

cat -n backend/src/microservices/saas-microservice/use-cases/update-hosted-connection-password.use.case.ts | head -50

Repository: rocket-admin/rocketadmin

Length of output: 2199


🏁 Script executed:

cat -n backend/src/entities/connection/connection.entity.ts

Repository: rocket-admin/rocketadmin

Length of output: 10808


🏁 Script executed:

# Check for database migrations or constraints
fd -t f "migration" backend/src --type f | head -10

Repository: rocket-admin/rocketadmin

Length of output: 124


🏁 Script executed:

head -200 backend/src/migrations/1594021894551-InitialMigration.ts | grep -A 5 -B 5 "connection\|unique\|UNIQUE" -i

Repository: rocket-admin/rocketadmin

Length of output: 4151


🏁 Script executed:

# Also check if there are any other migration files that might define this constraint
find backend/src/migrations -type f -name "*.ts" | xargs grep -l "connection\|database" | head -5

Repository: rocket-admin/rocketadmin

Length of output: 373


🏁 Script executed:

# Check for companyId in the connection table and any unique constraints added later
grep -n "companyId\|UNIQUE.*database\|UNIQUE.*company" backend/src/migrations/*.ts

Repository: rocket-admin/rocketadmin

Length of output: 8417


🏁 Script executed:

# Also check the create-connection-for-hosted-db to see the full picture
cat -n backend/src/microservices/saas-microservice/use-cases/create-connection-for-hosted-db.use.case.ts

Repository: rocket-admin/rocketadmin

Length of output: 5350


Potential ambiguity if multiple connections share the same database name.

The lookup uses only databaseName to identify the connection within a company. If a company has multiple connections with the same database value (e.g., pointing to different hosts), Array.find() returns only the first match, potentially updating the wrong connection.

The database schema does not enforce a unique constraint on (companyId, databaseName). Consider either:

  • Adding hostname to the DTO for disambiguation, or
  • Enforcing (companyId, databaseName) as unique at the database level.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/microservices/saas-microservice/use-cases/update-hosted-connection-password.use.case.ts`
around lines 34 - 37, The current lookup using companyConnections.find((conn) =>
conn.database === databaseName) can pick the wrong record when multiple
connections share the same database name; change the use case to disambiguate by
requiring and validating a hostname: update the incoming DTO to include
hostname, validate it's present, then replace the find predicate with one that
matches both conn.database === databaseName && conn.host === hostname (or
conn.hostname), and keep throwing
NotFoundException(Messages.CONNECTION_NOT_FOUND) when no exact match is found;
also update any callers/tests to supply the hostname.


connection.password = password;
await this._dbContext.connectionRepository.saveUpdatedConnection(connection);

return { success: true };
}
}
Loading