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
2 changes: 2 additions & 0 deletions backend/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Controller, Get, Inject, UseInterceptors } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { UseCaseType } from './common/data-injection.tokens.js';
import { Timeout } from './decorators/timeout.decorator.js';
import { InTransactionEnum } from './enums/index.js';
import { SentryInterceptor } from './interceptors/index.js';
import { IGetHello } from './use-cases-app/use-cases-app.interface.js';

@UseInterceptors(SentryInterceptor)
@Timeout()
@Controller()
@ApiTags('app')
export class AppController {
Expand Down
8 changes: 2 additions & 6 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { APP_GUARD } from '@nestjs/core';
import { ScheduleModule } from '@nestjs/schedule';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { AppController } from './app.controller.js';
Expand Down Expand Up @@ -36,7 +36,7 @@ import { UserActionModule } from './entities/user-actions/user-action.module.js'
import { UserSecretModule } from './entities/user-secret/user-secret.module.js';
import { SignInAuditModule } from './entities/user-sign-in-audit/sign-in-audit.module.js';
import { TableWidgetModule } from './entities/widget/table-widget.module.js';
import { TimeoutInterceptor } from './interceptors/index.js';

import { SaaSGatewayModule } from './microservices/gateways/saas-gateway.ts/saas-gateway.module.js';
import { SaasModule } from './microservices/saas-microservice/saas.module.js';
import { AppLoggerMiddleware } from './middlewares/logging-middleware/app-logger-middlewate.js';
Expand Down Expand Up @@ -101,10 +101,6 @@ import { DashboardWidgetModule } from './entities/visualizations/dashboard-widge
],
controllers: [AppController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: TimeoutInterceptor,
},
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
Expand Down
1 change: 1 addition & 0 deletions backend/src/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { BodyUuid } from './body-uuid.decorator.js';
export { QueryUuid } from './query-uuid.decorator.js';
export { BodyEmail } from './body-email.decorator.js';
export { QueryTableName } from './query-table-name.decorator.js';
export { Timeout, TIMEOUT_KEY, TimeoutDefaults } from './timeout.decorator.js';
17 changes: 17 additions & 0 deletions backend/src/decorators/timeout.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SetMetadata, applyDecorators, UseInterceptors } from '@nestjs/common';
import { TimeoutInterceptor } from '../interceptors/timeout.interceptor.js';

export const TIMEOUT_KEY = 'custom_timeout';

export const TimeoutDefaults = {
DEFAULT: 15000,
DEFAULT_TEST: 200000,
EXTENDED: 60000,
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The EXTENDED_TEST timeout value is too high (300000ms = 5 minutes) compared to the EXTENDED value (60000ms = 1 minute). This is inconsistent with the DEFAULT/DEFAULT_TEST ratio where TEST is ~13x the normal value. Consider adjusting EXTENDED_TEST to match the pattern, or if this is intentional, add a comment explaining why.

Suggested change
EXTENDED: 60000,
EXTENDED: 60000,
// Note: EXTENDED_TEST is intentionally capped at 5 minutes (5x EXTENDED) to avoid excessively long test runs.

Copilot uses AI. Check for mistakes.
EXTENDED_TEST: 300000,
AI: 300000,
AI_TEST: 600000,
} as const;

export function Timeout(timeoutMs?: number): MethodDecorator & ClassDecorator {
return applyDecorators(SetMetadata(TIMEOUT_KEY, timeoutMs), UseInterceptors(TimeoutInterceptor));
}
4 changes: 4 additions & 0 deletions backend/src/entities/ai/user-ai-requests-v2.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { UseCaseType } from '../../common/data-injection.tokens.js';
import { MasterPassword } from '../../decorators/master-password.decorator.js';
import { QueryTableName } from '../../decorators/query-table-name.decorator.js';
import { SlugUuid } from '../../decorators/slug-uuid.decorator.js';
import { Timeout, TimeoutDefaults } from '../../decorators/timeout.decorator.js';
import { UserId } from '../../decorators/user-id.decorator.js';
import { InTransactionEnum } from '../../enums/in-transaction.enum.js';
import { ConnectionEditGuard } from '../../guards/connection-edit.guard.js';
Expand Down Expand Up @@ -52,6 +53,7 @@ export class UserAIRequestsControllerV2 {
@ApiBody({ type: RequestInfoFromTableBodyDTO })
@ApiQuery({ name: 'tableName', required: true, type: String })
@ApiQuery({ name: 'threadId', required: false, type: String })
@Timeout(process.env.NODE_ENV !== 'test' ? TimeoutDefaults.AI : TimeoutDefaults.AI_TEST)
@Post('/ai/v2/request/:connectionId')
public async requestInfoFromTableWithAI(
@SlugUuid('connectionId') connectionId: string,
Expand Down Expand Up @@ -93,6 +95,7 @@ export class UserAIRequestsControllerV2 {
@ApiBody({ type: RequestInfoFromTableBodyDTO })
@ApiQuery({ name: 'tableName', required: true, type: String })
@ApiQuery({ name: 'threadId', required: false, type: String })
@Timeout(process.env.NODE_ENV !== 'test' ? TimeoutDefaults.AI : TimeoutDefaults.AI_TEST)
@Post('/ai/v3/request/:connectionId')
public async requestInfoFromTableWithAIBedrock(
@SlugUuid('connectionId') connectionId: string,
Expand Down Expand Up @@ -131,6 +134,7 @@ export class UserAIRequestsControllerV2 {
description: 'AI settings and widgets creation job has been queued.',
})
@UseGuards(ConnectionEditGuard)
@Timeout(process.env.NODE_ENV !== 'test' ? TimeoutDefaults.AI : TimeoutDefaults.AI_TEST)
@Get('/ai/v2/setup/:connectionId')
public async requestAISettingsAndWidgetsCreation(
@SlugUuid('connectionId') connectionId: string,
Expand Down
148 changes: 75 additions & 73 deletions backend/src/entities/api-key/api-key.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,89 +11,91 @@ import { FoundApiKeyDto } from './application/dto/found-api-key.dto.js';
import { ICreateApiKey, IDeleteApiKey, IGetApiKey, IGetApiKeys } from './use-cases/api-key-use-cases.interface.js';
import { buildCreatedApiKeyDto } from './utils/build-created-api-key.dto.js';
import { buildFoundApiKeyDto } from './utils/build-found-api-key.dto.js';
import { Timeout } from '../../decorators/timeout.decorator.js';

@UseInterceptors(SentryInterceptor)
@Timeout()
@Controller()
@ApiBearerAuth()
@ApiTags('Api Key')
@Injectable()
export class ApiKeyController {
constructor(
@Inject(UseCaseType.CREATE_API_KEY)
private readonly createApiKeyUseCase: ICreateApiKey,
@Inject(UseCaseType.GET_API_KEYS)
private readonly getApiKeysUseCase: IGetApiKeys,
@Inject(UseCaseType.GET_API_KEY)
private readonly getApiKeyUseCase: IGetApiKey,
@Inject(UseCaseType.DELETE_API_KEY)
private readonly deleteApiKeyUseCase: IDeleteApiKey,
) {}
constructor(
@Inject(UseCaseType.CREATE_API_KEY)
private readonly createApiKeyUseCase: ICreateApiKey,
@Inject(UseCaseType.GET_API_KEYS)
private readonly getApiKeysUseCase: IGetApiKeys,
@Inject(UseCaseType.GET_API_KEY)
private readonly getApiKeyUseCase: IGetApiKey,
@Inject(UseCaseType.DELETE_API_KEY)
private readonly deleteApiKeyUseCase: IDeleteApiKey,
) {}

@ApiOperation({ summary: 'Create new API key' })
@ApiResponse({
status: 200,
description: 'Api key created.',
type: CreatedApiKeyDto,
})
@Post('/apikey')
public async createApiKey(@UserId() userId: string, @Body() apiKeyData: CreateApiKeyDto): Promise<CreatedApiKeyDto> {
const apiKey = await this.createApiKeyUseCase.execute(
{
userId,
title: apiKeyData.title,
},
InTransactionEnum.ON,
);
return buildCreatedApiKeyDto(apiKey);
}
@ApiOperation({ summary: 'Create new API key' })
@ApiResponse({
status: 200,
description: 'Api key created.',
type: CreatedApiKeyDto,
})
@Post('/apikey')
public async createApiKey(@UserId() userId: string, @Body() apiKeyData: CreateApiKeyDto): Promise<CreatedApiKeyDto> {
const apiKey = await this.createApiKeyUseCase.execute(
{
userId,
title: apiKeyData.title,
},
InTransactionEnum.ON,
);
return buildCreatedApiKeyDto(apiKey);
}

@ApiOperation({ summary: 'Get all user api keys' })
@ApiResponse({
status: 200,
description: 'Get all user api keys.',
type: FoundApiKeyDto,
isArray: true,
})
@Get('/apikeys')
public async getApiKeys(@UserId() userId: string): Promise<Array<FoundApiKeyDto>> {
const foundApiKeys = await this.getApiKeysUseCase.execute(userId);
return foundApiKeys.map((apiKey) => buildFoundApiKeyDto(apiKey));
}
@ApiOperation({ summary: 'Get all user api keys' })
@ApiResponse({
status: 200,
description: 'Get all user api keys.',
type: FoundApiKeyDto,
isArray: true,
})
@Get('/apikeys')
public async getApiKeys(@UserId() userId: string): Promise<Array<FoundApiKeyDto>> {
const foundApiKeys = await this.getApiKeysUseCase.execute(userId);
return foundApiKeys.map((apiKey) => buildFoundApiKeyDto(apiKey));
}

@ApiOperation({ summary: 'Get api key by id' })
@ApiResponse({
status: 200,
description: 'Get api key by id.',
type: FoundApiKeyDto,
})
@Get('/apikey/:apiKeyId')
public async getApiKey(@UserId() userId: string, @SlugUuid('apiKeyId') apiKeyId: string): Promise<FoundApiKeyDto> {
const foundApiKey = await this.getApiKeyUseCase.execute({ userId, apiKeyId });
return buildFoundApiKeyDto(foundApiKey);
}
@ApiOperation({ summary: 'Get api key by id' })
@ApiResponse({
status: 200,
description: 'Get api key by id.',
type: FoundApiKeyDto,
})
@Get('/apikey/:apiKeyId')
public async getApiKey(@UserId() userId: string, @SlugUuid('apiKeyId') apiKeyId: string): Promise<FoundApiKeyDto> {
const foundApiKey = await this.getApiKeyUseCase.execute({ userId, apiKeyId });
return buildFoundApiKeyDto(foundApiKey);
}

@ApiOperation({ summary: 'Delete api key by id' })
@ApiResponse({
status: 200,
description: 'Get api key by id.',
type: FoundApiKeyDto,
})
@Delete('/apikey/:apiKeyId')
public async deleteApiKey(@UserId() userId: string, @SlugUuid('apiKeyId') apiKeyId: string): Promise<FoundApiKeyDto> {
const deletedApiKey = await this.deleteApiKeyUseCase.execute({ userId, apiKeyId }, InTransactionEnum.ON);
return buildFoundApiKeyDto(deletedApiKey);
}
@ApiOperation({ summary: 'Delete api key by id' })
@ApiResponse({
status: 200,
description: 'Get api key by id.',
type: FoundApiKeyDto,
})
@Delete('/apikey/:apiKeyId')
public async deleteApiKey(@UserId() userId: string, @SlugUuid('apiKeyId') apiKeyId: string): Promise<FoundApiKeyDto> {
const deletedApiKey = await this.deleteApiKeyUseCase.execute({ userId, apiKeyId }, InTransactionEnum.ON);
return buildFoundApiKeyDto(deletedApiKey);
}

@ApiOperation({ summary: 'Check api key' })
@ApiResponse({
status: 200,
description: 'Api key is valid.',
})
@Get('/check/apikey')
public async checkApiKey(): Promise<any> {
return {
result: true,
message: 'Api key is valid',
};
}
@ApiOperation({ summary: 'Check api key' })
@ApiResponse({
status: 200,
description: 'Api key is valid.',
})
@Get('/check/apikey')
public async checkApiKey(): Promise<any> {
return {
result: true,
message: 'Api key is valid',
};
}
}
2 changes: 2 additions & 0 deletions backend/src/entities/company-info/company-info.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ import { FoundCompanyWhiteLabelPropertiesRO } from './application/dto/found-comp
import { PaidFeatureGuard } from '../../guards/paid-feature.guard.js';
import { isTest } from '../../helpers/app/is-test.js';
import { TurnstileService } from '../../shared/services/turnstile.service.js';
import { Timeout } from '../../decorators/timeout.decorator.js';

@UseInterceptors(SentryInterceptor)
@Timeout()
@Controller('company')
@ApiBearerAuth()
@ApiTags('Company')
Expand Down
Loading
Loading