Skip to content
4 changes: 2 additions & 2 deletions backend/src/common/application/global-database-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { ConnectionPropertiesEntity } from '../../entities/connection-properties
import { IConnectionPropertiesRepository } from '../../entities/connection-properties/repository/connection-properties.repository.interface.js';
import { customConnectionPropertiesRepositoryExtension } from '../../entities/connection-properties/repository/custom-connection-properties-repository-extension.js';
import { CustomFieldsEntity } from '../../entities/custom-field/custom-fields.entity.js';
import { cusomFieldsCustomRepositoryExtension } from '../../entities/custom-field/repository/custom-field-repository-extension.js';
import { customFieldsCustomRepositoryExtension } from '../../entities/custom-field/repository/custom-field-repository-extension.js';
import { ICustomFieldsRepository } from '../../entities/custom-field/repository/custom-fields-repository.interface.js';
import { EmailVerificationEntity } from '../../entities/email/email-verification.entity.js';
import { IEmailVerificationRepository } from '../../entities/email/repository/email-verification.repository.interface.js';
Expand Down Expand Up @@ -192,7 +192,7 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext {
.extend(customConnectionPropertiesRepositoryExtension);
this._customFieldsRepository = this.appDataSource
.getRepository(CustomFieldsEntity)
.extend(cusomFieldsCustomRepositoryExtension);
.extend(customFieldsCustomRepositoryExtension);
this._tableLogsRepository = this.appDataSource
.getRepository(TableLogsEntity)
.extend(tableLogsCustomRepositoryExtension);
Expand Down
13 changes: 12 additions & 1 deletion backend/src/entities/agent/agent.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,26 @@ export class AgentEntity {
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt: Date;

private _tokenChanged = false;

setToken(token: string): void {
this.token = token;
this._tokenChanged = true;
}

@BeforeInsert()
encryptToken(): void {
this.token = Encryptor.hashDataHMAC(this.token);
this._tokenChanged = false;
}

@BeforeUpdate()
updateTimestampAndEncryptToken(): void {
this.updatedAt = new Date();
this.token = Encryptor.hashDataHMAC(this.token);
if (this._tokenChanged) {
this.token = Encryptor.hashDataHMAC(this.token);
this._tokenChanged = false;
}
Comment on lines +28 to +47
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

updateTimestampAndEncryptToken() now hashes only when _tokenChanged is set via setToken(). Since token remains a public field, any direct assignment like agent.token = newToken will bypass hashing and can result in plaintext tokens being stored on update. Consider making token effectively write-protected (e.g., private token + getter) and/or tracking changes via @AfterLoad/previous-value comparison so hashing cannot be skipped.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +47
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify all token assignments use setToken() method
# Expected: Only setToken() should be used for token assignment, not direct property access

rg -n '\.token\s*=' --type=ts -g '**/agent/**' -g '!*.entity.ts' -C2

Repository: rocket-admin/rocketadmin

Length of output: 588


Replace direct token assignment with setToken() method to ensure proper hashing on subsequent updates.

The _tokenChanged flag pattern is correct for preventing double-hashing. However, direct assignment to the token property (savedAgent.token = token) bypasses this mechanism. This happens in backend/src/entities/agent/repository/custom-agent-repository-extension.ts (line 19), where after the agent is saved and the token is hashed during @BeforeInsert, the raw token is directly assigned. On the next update, since _tokenChanged remains false, the token will not be re-hashed and the plaintext value will be stored.

Use setToken() instead:

- savedAgent.token = token;
+ savedAgent.setToken(token);

Ensure all token assignments throughout the codebase use setToken() rather than direct property access.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/entities/agent/agent.entity.ts` around lines 28 - 47, Direct
assignment to the token property bypasses the _tokenChanged flag and the
`@BeforeInsert/`@BeforeUpdate hooks (encryptToken,
updateTimestampAndEncryptToken), causing plaintext tokens to be saved; replace
any direct token writes (e.g., savedAgent.token = token in
custom-agent-repository-extension.ts) with savedAgent.setToken(token) so the
setter sets _tokenChanged and ensures hashing on the next lifecycle hook; audit
the codebase for other direct token assignments and use setToken() instead,
leaving reads unchanged.

}

@OneToOne(
Expand Down
9 changes: 2 additions & 7 deletions backend/src/entities/agent/agent.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthMiddleware } from '../../authorization/index.js';
import { GlobalDatabaseContext } from '../../common/application/global-database-context.js';
import { BaseType } from '../../common/data-injection.tokens.js';
import { LogOutEntity } from '../log-out/log-out.entity.js';
Expand All @@ -17,8 +16,4 @@ import { AgentEntity } from './agent.entity.js';
],
exports: [],
})
export class AgentModule implements NestModule {
public configure(consumer: MiddlewareConsumer): any {
consumer.apply(AuthMiddleware).forRoutes();
}
}
export class AgentModule {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ConnectionTypeTestEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js';
import { ConnectionEntity } from '../../connection/connection.entity.js';
import { AgentEntity } from '../agent.entity.js';

Expand All @@ -7,4 +8,6 @@ export interface IAgentRepository {
createNewAgentForConnectionAndReturnToken(connection: ConnectionEntity): Promise<string>;

renewOrCreateConnectionToken(connectionId: string): Promise<string>;

getTestAgentToken(connectionType: ConnectionTypeTestEnum): string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,18 @@ import { ConnectionTypeTestEnum } from '@rocketadmin/shared-code/dist/src/shared
import { nanoid } from 'nanoid';
import { ConnectionEntity } from '../../connection/connection.entity.js';
import { AgentEntity } from '../agent.entity.js';
import { IAgentRepository } from './agent.repository.interface.js';

export const customAgentRepositoryExtension = {
async saveNewAgent(agent: AgentEntity): Promise<AgentEntity> {
return await this.save(agent);
},

export const customAgentRepositoryExtension: IAgentRepository = {
async createNewAgentForConnectionAndReturnToken(connection: ConnectionEntity): Promise<string> {
const newAgent = await this.createNewAgentForConnection(connection);
return newAgent.token;
},

async createNewAgentForConnection(connection: ConnectionEntity): Promise<AgentEntity> {
const agent = new AgentEntity();
let token = nanoid(64);
if (process.env.NODE_ENV !== 'test') {
agent.token = token;
} else {
token = this.getTestAgentToken(connection.type);
agent.token = token;
}
const token = process.env.NODE_ENV !== 'test' ? nanoid(64) : this.getTestAgentToken(connection.type);
agent.setToken(token);
agent.connection = connection;
const savedAgent = await this.save(agent);
savedAgent.token = token;
Expand All @@ -42,7 +34,7 @@ export const customAgentRepositoryExtension = {
return await this.createNewAgentForConnectionAndReturnToken(foundConnection);
} else {
const newToken = nanoid(64);
foundAgent.token = newToken;
foundAgent.setToken(newToken);
await this.save(foundAgent);
return newToken;
}
Expand Down
64 changes: 41 additions & 23 deletions backend/src/entities/amplitude/amplitude.service.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,56 @@
import Amplitude from '@amplitude/node';
import { Injectable } from '@nestjs/common';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AmplitudeEventTypeEnum } from '../../enums/index.js';
import { UserEntity } from '../user/user.entity.js';

export interface AmplitudeLogOptions {
user_email?: string;
tablesCount?: number;
reason?: string;
message?: string;
operationCount?: number;
}

@Injectable()
export class AmplitudeService {
export class AmplitudeService implements OnModuleInit {
private client: ReturnType<typeof Amplitude.init>;
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

Type annotation should reflect that client can be undefined.

The client property is only initialized when AMPLITUDE_API_KEY is set, but the type annotation doesn't reflect this. While sendLog handles this with an early return check, the type should explicitly include undefined for type safety.

Proposed fix
-	private client: ReturnType<typeof Amplitude.init>;
+	private client: ReturnType<typeof Amplitude.init> | undefined;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private client: ReturnType<typeof Amplitude.init>;
private client: ReturnType<typeof Amplitude.init> | undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/entities/amplitude/amplitude.service.ts` at line 18, The client
property is typed as ReturnType<typeof Amplitude.init> but it may remain
uninitialized when AMPLITUDE_API_KEY is not set; update the
AmplitudeService.client type to include undefined (e.g., ReturnType<typeof
Amplitude.init> | undefined) so the declaration matches runtime behavior,
leaving the existing early-return check in sendLog intact; locate the client
field declaration and adjust its type to allow undefined.


constructor(
@InjectRepository(UserEntity)
private readonly userRepository: Repository<UserEntity>,
) {}

public async formAndSendLogRecord(event_type: AmplitudeEventTypeEnum, user_id: string, options = null) {
public onModuleInit(): void {
if (process.env.AMPLITUDE_API_KEY) {
this.client = Amplitude.init(process.env.AMPLITUDE_API_KEY);
}
}

public async formAndSendLogRecord(
event_type: AmplitudeEventTypeEnum,
user_id: string,
options?: AmplitudeLogOptions,
): Promise<void> {
try {
if (process.env.NODE_ENV === 'test') return;
let user_email = (await this.userRepository.findOne({ where: { id: user_id } }))?.email;
if (!user_email && options) {
user_email = options?.user_email;
user_email = options.user_email;
}
let event_properties;
let event_properties: Record<string, unknown> | undefined;
if (user_email) {
event_properties = {
user_properties: {
email: user_email ? user_email : 'unknown',
tablesCount: options?.tablesCount ? options.tablesCount : undefined,
reason: options?.reason ? options?.reason : undefined,
message: options?.message ? options.message : undefined,
email: user_email ?? 'unknown',
tablesCount: options?.tablesCount,
reason: options?.reason,
message: options?.message,
},
};
}
if (options?.operationCount && options?.operationCount > 0) {
if (options?.operationCount && options.operationCount > 0) {
const promisesArr = Array.from(Array(options.operationCount), () =>
this.sendLog(event_type, user_id, event_properties),
);
Expand All @@ -43,21 +63,19 @@ export class AmplitudeService {
}
}

private async sendLog(eventType, cognitoUserName, eventProperties) {
const client = Amplitude.init(process.env.AMPLITUDE_API_KEY);
private async sendLog(
eventType: AmplitudeEventTypeEnum,
userId: string,
eventProperties?: Record<string, unknown>,
): Promise<void> {
if (!this.client) return;
try {
client
.logEvent({
event_type: eventType,
user_id: cognitoUserName,
event_properties: eventProperties ? eventProperties : undefined,
})
.catch((e) => {
throw new Error(e);
});
client.flush().catch((e) => {
throw new Error(e);
await this.client.logEvent({
event_type: eventType,
user_id: userId,
event_properties: eventProperties,
});
await this.client.flush();
} catch (e) {
console.error(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ export class CompanyInfoHelperService {

const companyInformationFromSaaS = await this.saasCompanyGatewayService.getCompanyInfo(companyId);

const countUsersInCompany = await this._dbContext.userRepository.countUsersInCompany(companyId);
const countInvitationsInCompany =
await this._dbContext.invitationInCompanyRepository.countNonExpiredInvitationsInCompany(companyId);
const [countUsersInCompany, countInvitationsInCompany] = await Promise.all([
this._dbContext.userRepository.countUsersInCompany(companyId),
this._dbContext.invitationInCompanyRepository.countNonExpiredInvitationsInCompany(companyId),
]);

if (companyInformationFromSaaS.subscriptionLevel === SubscriptionLevelEnum.FREE_PLAN) {
return countUsersInCompany + countInvitationsInCompany < Constants.FREE_PLAN_USERS_COUNT;
Expand Down
3 changes: 2 additions & 1 deletion backend/src/entities/company-info/company-info.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
IRemoveUserFromCompany,
IRevokeUserInvitationInCompany,
ISuspendUsersInCompany,
IUnsuspendUsersInCompany,
IToggleCompanyTestConnectionsMode,
IUpdateCompanyName,
IUpdateUsers2faStatusInCompany,
Expand Down Expand Up @@ -128,7 +129,7 @@ export class CompanyInfoController {
@Inject(UseCaseType.SUSPEND_USERS_IN_COMPANY)
private readonly suspendUsersInCompanyUseCase: ISuspendUsersInCompany,
@Inject(UseCaseType.UNSUSPEND_USERS_IN_COMPANY)
private readonly unSuspendUsersInCompanyUseCase: ISuspendUsersInCompany,
private readonly unSuspendUsersInCompanyUseCase: IUnsuspendUsersInCompany,
@Inject(UseCaseType.TOGGLE_TEST_CONNECTIONS_DISPLAY_MODE_IN_COMPANY)
private readonly toggleTestConnectionsCompanyDisplayModeUseCase: IToggleCompanyTestConnectionsMode,
@Inject(UseCaseType.UPLOAD_COMPANY_LOGO)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const invitationInCompanyCustomRepositoryExtension: IInvitationInCompanyR
.leftJoinAndSelect('invitation_in_company.company', 'company')
.leftJoinAndSelect('company.users', 'users')
.where("invitation_in_company.createdAt > NOW() - INTERVAL '1 day'")
.where('invitation_in_company.verification_string = :verificationString', { verificationString });
.andWhere('invitation_in_company.verification_string = :verificationString', { verificationString });
return await qb.getOne();
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,6 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {
.getOne();
},

async finOneCompanyInfoByUserId(userId: string): Promise<CompanyInfoEntity> {
return await this.createQueryBuilder('company_info')
.leftJoinAndSelect('company_info.users', 'users')
.where('users.id = :userId', { userId })
.getOne();
},

async findCompanyInfoByUserId(userId: string): Promise<CompanyInfoEntity> {
return await this.createQueryBuilder('company_info')
.leftJoinAndSelect('company_info.users', 'users')
Expand Down Expand Up @@ -94,9 +87,10 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {
.andWhere('connections.isTestConnection IS FALSE')
.andWhere('connections.is_frozen IS FALSE')
.getMany();
return foundCompaniesWithPaidConnections.map((companyInfo: CompanyInfoEntity) => {
return companyInfo.connections;
});
return foundCompaniesWithPaidConnections
.map((companyInfo: CompanyInfoEntity) => companyInfo.connections)
.filter(Boolean)
.flat();
},

async findCompanyFrozenPaidConnections(companyIds: Array<string>): Promise<Array<ConnectionEntity>> {
Expand All @@ -108,9 +102,10 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {
.andWhere('connections.isTestConnection IS FALSE')
.andWhere('connections.is_frozen IS TRUE')
.getMany();
return foundCompaniesWithPaidConnections.map((companyInfo: CompanyInfoEntity) => {
return companyInfo.connections;
});
return foundCompaniesWithPaidConnections
.map((companyInfo: CompanyInfoEntity) => companyInfo.connections)
.filter(Boolean)
.flat();
},

async findCompanyWithLogo(companyId: string): Promise<CompanyInfoEntity> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ export interface ICompanyInfoRepository {

findOneCompanyInfoByUserIdWithConnections(userId: string): Promise<CompanyInfoEntity>;

finOneCompanyInfoByUserId(userId: string): Promise<CompanyInfoEntity>;

findCompanyInfoByUserId(userId: string): Promise<CompanyInfoEntity>;

findFullCompanyInfoByUserId(userId: string): Promise<CompanyInfoEntity>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ export interface ISuspendUsersInCompany {
execute(inputData: SuspendUsersInCompanyDS, inTransaction: InTransactionEnum): Promise<SuccessResponse>;
}

export type IUnsuspendUsersInCompany = ISuspendUsersInCompany;

export interface IToggleCompanyTestConnectionsMode {
execute(inputData: ToggleTestConnectionDisplayModeDs, inTransaction: InTransactionEnum): Promise<SuccessResponse>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class ToggleCompanyTestConnectionsDisplayModeUseCase

public async implementation(inputData: ToggleTestConnectionDisplayModeDs): Promise<SuccessResponse> {
const { userId, displayMode } = inputData;
const foundCompanyInfo = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(userId);
const foundCompanyInfo = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(userId);
if (!foundCompanyInfo) {
throw new NotFoundException(Messages.COMPANY_NOT_FOUND);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
export class CreateConnectionPropertiesDs {
hidden_tables: Array<string>;
userId: string;
connectionId: string;
master_password: string;
logo_url: string;
primary_color: string;
secondary_color: string;
hostname: string;
company_name: string;
tables_audit: boolean;
human_readable_table_names: boolean;
allow_ai_requests: boolean;
default_showing_table: string;
table_categories: Array<{
hidden_tables?: Array<string>;
logo_url?: string;
primary_color?: string;
secondary_color?: string;
hostname?: string;
company_name?: string;
tables_audit?: boolean;
human_readable_table_names?: boolean;
allow_ai_requests?: boolean;
default_showing_table?: string;
table_categories?: Array<{
category_name: string;
tables: Array<string>;
category_color: string;
Expand Down
Loading
Loading