Skip to content

Commit a293617

Browse files
authored
fix(api-service): bugs found in bug bash in regarding to 4 tiers (#7775)
1 parent caa46e3 commit a293617

File tree

92 files changed

+4061
-626
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+4061
-626
lines changed

.source

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
3+
export class ApiKeyDto {
4+
@ApiProperty({
5+
type: String,
6+
description: 'API key',
7+
example: 'sk_test_1234567890abcdef',
8+
})
9+
key: string;
10+
11+
@ApiProperty({
12+
type: String,
13+
description: 'User ID associated with the API key',
14+
example: '60d5ecb8b3b3a30015f3e1a4',
15+
})
16+
_userId: string;
17+
18+
@ApiPropertyOptional({
19+
type: String,
20+
description: 'Hashed representation of the API key',
21+
example: 'hash_value_here',
22+
})
23+
hash?: string;
24+
}

apps/api/src/app/environments-v1/dtos/create-environment-request.dto.ts

+20-8
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,30 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
22
import { IsDefined, IsHexColor, IsMongoId, IsOptional, IsString } from 'class-validator';
33

44
export class CreateEnvironmentRequestDto {
5-
@ApiProperty()
6-
@IsDefined()
7-
@IsString()
5+
@ApiProperty({
6+
type: String,
7+
description: 'Name of the environment to be created',
8+
example: 'Production Environment',
9+
})
10+
@IsDefined({ message: 'Environment name is required' })
11+
@IsString({ message: 'Environment name must be a string' })
812
name: string;
913

10-
@ApiPropertyOptional()
14+
@ApiPropertyOptional({
15+
type: String,
16+
description: 'MongoDB ObjectId of the parent environment (optional)',
17+
example: '60d5ecb8b3b3a30015f3e1a1',
18+
})
1119
@IsOptional()
12-
@IsMongoId()
20+
@IsMongoId({ message: 'Parent ID must be a valid MongoDB ObjectId' })
1321
parentId?: string;
1422

15-
@ApiProperty()
16-
@IsDefined()
17-
@IsHexColor()
23+
@ApiProperty({
24+
type: String,
25+
description: 'Hex color code for the environment',
26+
example: '#3498db',
27+
})
28+
@IsDefined({ message: 'Environment color is required' })
29+
@IsHexColor({ message: 'Color must be a valid hex color code' })
1830
color: string;
1931
}

apps/api/src/app/environments-v1/dtos/environment-response.dto.ts

+42-19
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,53 @@
11
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import { ApiKeyDto } from './api-key.dto';
23

34
export class EnvironmentResponseDto {
4-
@ApiPropertyOptional()
5-
_id?: string;
6-
7-
@ApiProperty()
5+
@ApiProperty({
6+
type: String,
7+
description: 'Unique identifier of the environment',
8+
example: '60d5ecb8b3b3a30015f3e1a1',
9+
})
10+
_id: string;
11+
12+
@ApiProperty({
13+
type: String,
14+
description: 'Name of the environment',
15+
example: 'Production Environment',
16+
})
817
name: string;
918

10-
@ApiProperty()
19+
@ApiProperty({
20+
type: String,
21+
description: 'Organization ID associated with the environment',
22+
example: '60d5ecb8b3b3a30015f3e1a2',
23+
})
1124
_organizationId: string;
1225

13-
@ApiProperty()
26+
@ApiProperty({
27+
type: String,
28+
description: 'Unique identifier for the environment',
29+
example: 'prod-env-01',
30+
})
1431
identifier: string;
1532

16-
@ApiPropertyOptional()
17-
apiKeys?: IApiKeyDto[];
18-
19-
@ApiProperty()
20-
_parentId: string;
21-
22-
@ApiPropertyOptional()
33+
@ApiPropertyOptional({
34+
type: ApiKeyDto,
35+
isArray: true,
36+
description: 'List of API keys associated with the environment',
37+
})
38+
apiKeys?: ApiKeyDto[];
39+
40+
@ApiPropertyOptional({
41+
type: String,
42+
description: 'Parent environment ID',
43+
example: '60d5ecb8b3b3a30015f3e1a3',
44+
})
45+
_parentId?: string;
46+
47+
@ApiPropertyOptional({
48+
type: String,
49+
description: 'URL-friendly slug for the environment',
50+
example: 'production',
51+
})
2352
slug?: string;
2453
}
25-
26-
export interface IApiKeyDto {
27-
key: string;
28-
_userId: string;
29-
hash?: string;
30-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ApiServiceLevelEnum } from '@novu/shared';
2+
import { UserSession } from '@novu/testing';
3+
import { expect } from 'chai';
4+
import { Novu } from '@novu/api';
5+
import { expectSdkExceptionGeneric, initNovuClassSdkInternalAuth } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper';
6+
7+
describe('Env Controller', async () => {
8+
let session: UserSession;
9+
let novuClient: Novu;
10+
before(async () => {
11+
session = new UserSession();
12+
await session.initialize({});
13+
novuClient = initNovuClassSdkInternalAuth(session);
14+
});
15+
describe('Create Env', () => {
16+
[ApiServiceLevelEnum.BUSINESS, ApiServiceLevelEnum.ENTERPRISE].forEach((serviceLevel) => {
17+
it(`should be able to create env in ${serviceLevel} tier`, async () => {
18+
await session.updateOrganizationServiceLevel(serviceLevel);
19+
const { name, environmentRequestDto } = generateRandomEnvRequest();
20+
const createdEnv = await novuClient.environments.create(environmentRequestDto);
21+
const { result } = createdEnv;
22+
expect(result).to.be.ok;
23+
expect(result.name).to.equal(name);
24+
});
25+
});
26+
27+
[ApiServiceLevelEnum.PRO, ApiServiceLevelEnum.FREE].forEach((serviceLevel) => {
28+
it(`should not be able to create env in ${serviceLevel} tier`, async () => {
29+
await session.updateOrganizationServiceLevel(serviceLevel);
30+
const { error, successfulBody } = await expectSdkExceptionGeneric(() =>
31+
novuClient.environments.create(generateRandomEnvRequest().environmentRequestDto)
32+
);
33+
expect(error).to.be.ok;
34+
expect(error?.message).to.equal('Payment Required');
35+
expect(error?.statusCode).to.equal(402);
36+
});
37+
});
38+
});
39+
function generateRandomEnvRequest() {
40+
const name = generateRandomName('env');
41+
const parentId = session.environment._id;
42+
const environmentRequestDto = {
43+
name,
44+
parentId,
45+
color: '#b15353',
46+
};
47+
48+
return { name, parentId, environmentRequestDto };
49+
}
50+
});
51+
function generateRandomName(prefix: string = 'env'): string {
52+
const timestamp = Date.now();
53+
const randomPart = Math.random().toString(36).substring(2, 7);
54+
55+
return `${prefix}-${randomPart}-${timestamp}`;
56+
}

apps/api/src/app/environments-v1/environments-v1.controller.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ import {
1010
UseGuards,
1111
UseInterceptors,
1212
} from '@nestjs/common';
13-
import { ApiExcludeController, ApiExcludeEndpoint, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
13+
import { ApiExcludeEndpoint, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
1414
import { Roles } from '@novu/application-generic';
1515
import { ApiAuthSchemeEnum, MemberRoleEnum, ProductFeatureKeyEnum, UserSessionData } from '@novu/shared';
1616
import { ExternalApiAccessible } from '../auth/framework/external-api.decorator';
1717
import { ProductFeature } from '../shared/decorators/product-feature.decorator';
1818
import { ApiKey } from '../shared/dtos/api-key';
1919
import { ApiCommonResponses, ApiResponse } from '../shared/framework/response.decorator';
2020
import { UserAuthentication } from '../shared/framework/swagger/api.key.security';
21-
import { SdkGroupName } from '../shared/framework/swagger/sdk.decorators';
21+
import { SdkGroupName, SdkMethodName } from '../shared/framework/swagger/sdk.decorators';
2222
import { UserSession } from '../shared/framework/user.decorator';
2323
import { CreateEnvironmentRequestDto } from './dtos/create-environment-request.dto';
2424
import { EnvironmentResponseDto } from './dtos/environment-response.dto';
@@ -36,6 +36,7 @@ import { RegenerateApiKeys } from './usecases/regenerate-api-keys/regenerate-api
3636
import { UpdateEnvironmentCommand } from './usecases/update-environment/update-environment.command';
3737
import { UpdateEnvironment } from './usecases/update-environment/update-environment.usecase';
3838
import { RolesGuard } from '../auth/framework/roles.guard';
39+
import { ErrorDto } from '../../error-dto';
3940

4041
/**
4142
* @deprecated use EnvironmentsControllerV2
@@ -45,7 +46,6 @@ import { RolesGuard } from '../auth/framework/roles.guard';
4546
@UseInterceptors(ClassSerializerInterceptor)
4647
@UserAuthentication()
4748
@ApiTags('Environments')
48-
@ApiExcludeController()
4949
export class EnvironmentsControllerV1 {
5050
constructor(
5151
private createEnvironmentUsecase: CreateEnvironment,
@@ -63,6 +63,7 @@ export class EnvironmentsControllerV1 {
6363
})
6464
@ApiResponse(EnvironmentResponseDto)
6565
@ExternalApiAccessible()
66+
@ApiExcludeEndpoint()
6667
async getCurrentEnvironment(@UserSession() user: UserSessionData): Promise<EnvironmentResponseDto> {
6768
return await this.getEnvironmentUsecase.execute(
6869
GetEnvironmentCommand.create({
@@ -77,11 +78,13 @@ export class EnvironmentsControllerV1 {
7778
@ApiOperation({
7879
summary: 'Create environment',
7980
})
80-
@ApiExcludeEndpoint()
8181
@ApiResponse(EnvironmentResponseDto, 201)
82+
@ApiResponse(ErrorDto, 402, false, false)
8283
@ProductFeature(ProductFeatureKeyEnum.MANAGE_ENVIRONMENTS)
8384
@UseGuards(RolesGuard)
8485
@Roles(MemberRoleEnum.ADMIN)
86+
@SdkGroupName('Environments')
87+
@SdkMethodName('create')
8588
async createEnvironment(
8689
@UserSession() user: UserSessionData,
8790
@Body() body: CreateEnvironmentRequestDto
@@ -103,6 +106,7 @@ export class EnvironmentsControllerV1 {
103106
})
104107
@ApiResponse(EnvironmentResponseDto, 200, true)
105108
@ExternalApiAccessible()
109+
@ApiExcludeEndpoint()
106110
async listMyEnvironments(@UserSession() user: UserSessionData): Promise<EnvironmentResponseDto[]> {
107111
return await this.getMyEnvironmentsUsecase.execute(
108112
GetMyEnvironmentsCommand.create({
@@ -146,6 +150,7 @@ export class EnvironmentsControllerV1 {
146150
@ApiResponse(ApiKey, 200, true)
147151
@ExternalApiAccessible()
148152
@SdkGroupName('Environments.ApiKeys')
153+
@ApiExcludeEndpoint()
149154
async listOrganizationApiKeys(@UserSession() user: UserSessionData): Promise<ApiKey[]> {
150155
const command = GetApiKeysCommand.create({
151156
userId: user._id,
@@ -160,6 +165,7 @@ export class EnvironmentsControllerV1 {
160165
@ApiResponse(ApiKey, 201, true)
161166
@UseGuards(RolesGuard)
162167
@Roles(MemberRoleEnum.ADMIN)
168+
@ApiExcludeEndpoint()
163169
async regenerateOrganizationApiKeys(@UserSession() user: UserSessionData): Promise<ApiKey[]> {
164170
const command = GetApiKeysCommand.create({
165171
userId: user._id,
@@ -178,6 +184,7 @@ export class EnvironmentsControllerV1 {
178184
@ProductFeature(ProductFeatureKeyEnum.MANAGE_ENVIRONMENTS)
179185
@UseGuards(RolesGuard)
180186
@Roles(MemberRoleEnum.ADMIN)
187+
@ApiExcludeEndpoint()
181188
async deleteEnvironment(@UserSession() user: UserSessionData, @Param('environmentId') environmentId: string) {
182189
return await this.deleteEnvironmentUsecase.execute(
183190
DeleteEnvironmentCommand.create({

apps/api/src/app/environments-v1/usecases/create-environment/create-environment.usecase.ts

+23-7
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import { createHash } from 'crypto';
33
import { nanoid } from 'nanoid';
44

55
import { encryptApiKey } from '@novu/application-generic';
6-
import { EnvironmentRepository, NotificationGroupRepository } from '@novu/dal';
6+
import { EnvironmentEntity, EnvironmentRepository, NotificationGroupRepository } from '@novu/dal';
77

88
import { EnvironmentEnum, PROTECTED_ENVIRONMENTS } from '@novu/shared';
99
import { CreateNovuIntegrationsCommand } from '../../../integrations/usecases/create-novu-integrations/create-novu-integrations.command';
1010
import { CreateNovuIntegrations } from '../../../integrations/usecases/create-novu-integrations/create-novu-integrations.usecase';
1111
import { CreateDefaultLayout, CreateDefaultLayoutCommand } from '../../../layouts/usecases';
1212
import { GenerateUniqueApiKey } from '../generate-unique-api-key/generate-unique-api-key.usecase';
1313
import { CreateEnvironmentCommand } from './create-environment.command';
14+
import { EnvironmentResponseDto } from '../../dtos/environment-response.dto';
1415

1516
@Injectable()
1617
export class CreateEnvironment {
@@ -22,11 +23,7 @@ export class CreateEnvironment {
2223
private createNovuIntegrationsUsecase: CreateNovuIntegrations
2324
) {}
2425

25-
async execute(command: CreateEnvironmentCommand) {
26-
const environmentCount = await this.environmentRepository.count({
27-
_organizationId: command.organizationId,
28-
});
29-
26+
async execute(command: CreateEnvironmentCommand): Promise<EnvironmentResponseDto> {
3027
const normalizedName = command.name.trim();
3128

3229
if (!command.system) {
@@ -108,9 +105,28 @@ export class CreateEnvironment {
108105
})
109106
);
110107

111-
return environment;
108+
return this.convertEnvironmentEntityToDto(environment);
112109
}
113110

111+
private convertEnvironmentEntityToDto(environment: EnvironmentEntity) {
112+
const dto = new EnvironmentResponseDto();
113+
114+
dto._id = environment._id;
115+
dto.name = environment.name;
116+
dto._organizationId = environment._organizationId;
117+
dto.identifier = environment.identifier;
118+
dto._parentId = environment._parentId;
119+
120+
if (environment.apiKeys && environment.apiKeys.length > 0) {
121+
dto.apiKeys = environment.apiKeys.map((apiKey) => ({
122+
key: apiKey.key,
123+
hash: apiKey.hash,
124+
_userId: apiKey._userId,
125+
}));
126+
}
127+
128+
return dto;
129+
}
114130
private getEnvironmentColor(name: string, commandColor?: string): string | undefined {
115131
if (name === EnvironmentEnum.DEVELOPMENT) return '#ff8547';
116132
if (name === EnvironmentEnum.PRODUCTION) return '#7e52f4';

apps/api/src/app/environments-v1/usecases/regenerate-api-keys/regenerate-api-keys.usecase.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { decryptApiKey, encryptApiKey } from '@novu/application-generic';
77
import { ApiException } from '../../../shared/exceptions/api.exception';
88
import { GenerateUniqueApiKey } from '../generate-unique-api-key/generate-unique-api-key.usecase';
99
import { GetApiKeysCommand } from '../get-api-keys/get-api-keys.command';
10-
import { IApiKeyDto } from '../../dtos/environment-response.dto';
10+
11+
import { ApiKeyDto } from '../../dtos/api-key.dto';
1112

1213
@Injectable()
1314
export class RegenerateApiKeys {
@@ -16,7 +17,7 @@ export class RegenerateApiKeys {
1617
private generateUniqueApiKey: GenerateUniqueApiKey
1718
) {}
1819

19-
async execute(command: GetApiKeysCommand): Promise<IApiKeyDto[]> {
20+
async execute(command: GetApiKeysCommand): Promise<ApiKeyDto[]> {
2021
const environment = await this.environmentRepository.findOne({ _id: command.environmentId });
2122

2223
if (!environment) {

apps/api/src/app/shared/helpers/e2e/sdk/e2e-sdk.helper.ts

+12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ export function initNovuClassSdk(session: UserSession, shouldRetry: boolean = fa
1717

1818
return new Novu(options);
1919
}
20+
export function initNovuClassSdkInternalAuth(session: UserSession, shouldRetry: boolean = false): Novu {
21+
const options: SDKOptions = {
22+
security: { bearerAuth: session.token },
23+
serverURL: session.serverUrl,
24+
debugLogger: console,
25+
};
26+
if (!shouldRetry) {
27+
options.retryConfig = { strategy: 'none' };
28+
}
29+
30+
return new Novu(options);
31+
}
2032
export function initNovuFunctionSdk(session: UserSession): NovuCore {
2133
return new NovuCore({ security: { secretKey: session.apiKey }, serverURL: session.serverUrl });
2234
}

0 commit comments

Comments
 (0)