Skip to content

Commit

Permalink
Merge branch 'next' into locale-instance
Browse files Browse the repository at this point in the history
  • Loading branch information
scopsy authored Jun 12, 2024
2 parents 4cbf1a3 + b256a71 commit 83e55ac
Show file tree
Hide file tree
Showing 84 changed files with 1,212 additions and 231 deletions.
1 change: 0 additions & 1 deletion .github/workflows/reusable-web-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ jobs:
# This workflow contains a single job called "build"
e2e_web:
strategy:
fail-fast: true
matrix:
# run 5 copies of the current job in parallel
containers: [1, 2, 3, 4, 5]
Expand Down
2 changes: 1 addition & 1 deletion .source
2 changes: 2 additions & 0 deletions apps/api/src/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,5 @@ IS_USE_MERGED_DIGEST_ID_ENABLED=true

HUBSPOT_INVITE_NUDGE_EMAIL_USER_LIST_ID=
HUBSPOT_PRIVATE_APP_ACCESS_TOKEN=

TUNNEL_BASE_ADDRESS=example.com
2 changes: 2 additions & 0 deletions apps/api/src/.example.env
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,5 @@ API_RATE_LIMIT_MAXIMUM_UNLIMITED_GLOBAL=

HUBSPOT_INVITE_NUDGE_EMAIL_USER_LIST_ID=
HUBSPOT_PRIVATE_APP_ACCESS_TOKEN=

TUNNEL_BASE_ADDRESS=
34 changes: 34 additions & 0 deletions apps/api/src/app/auth/e2e/user-registration.e2e-ee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { EnvironmentRepository } from '@novu/dal';
import { UserSession } from '@novu/testing';
import * as jwt from 'jsonwebtoken';
import { expect } from 'chai';
import { IJwtPayload } from '@novu/shared';

describe('User registration in enterprise - /auth/register (POST)', async () => {
let session: UserSession;
const environmentRepository = new EnvironmentRepository();

before(async () => {
session = new UserSession();
await session.initialize();
});

it('registered user should have the bridge url set on their environment', async () => {
const { body } = await session.testAgent.post('/v1/auth/register').send({
email: '[email protected]',
firstName: 'Test',
lastName: 'User',
password: '123@Qwerty',
organizationName: 'Sample org',
});

expect(body.data.token).to.be.ok;

const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload;

expect(jwtContent.environmentId).to.be.ok;
const environment = await environmentRepository.findOne({ _id: jwtContent.environmentId });

expect(environment.echo.url).to.be.ok;
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export class InBoundParseDomainDto {
inboundParseDomain?: string;
}

export class BridgeConfigurationDto {
@ApiPropertyOptional({ type: String })
url?: string;
}

export class UpdateEnvironmentRequestDto {
@ApiProperty()
@IsOptional()
Expand All @@ -26,4 +31,9 @@ export class UpdateEnvironmentRequestDto {
type: InBoundParseDomainDto,
})
dns?: InBoundParseDomainDto;

@ApiPropertyOptional({
type: BridgeConfigurationDto,
})
bridge?: BridgeConfigurationDto;
}
32 changes: 32 additions & 0 deletions apps/api/src/app/environments/e2e/regenerate-api-keys.e2e-ee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { UserSession } from '@novu/testing';
import { expect } from 'chai';
import { UpdateEnvironmentRequestDto } from '../dtos/update-environment-request.dto';

describe('Environment - Regenerate Api Key', async () => {
let session: UserSession;

before(async () => {
session = new UserSession();
await session.initialize();
});

it('should regenerate echo url on api key regeneration as well', async () => {
const updatePayload: UpdateEnvironmentRequestDto = {
name: 'Development',
bridge: { url: 'http://example.com' },
};

await session.testAgent.put(`/v1/environments/${session.environment._id}`).send(updatePayload).expect(200);

const firstResponse = await session.testAgent.get('/v1/environments/me');

const oldEchoUrl = firstResponse.body.data.echo.url;

await session.testAgent.post('/v1/environments/api-keys/regenerate').send({});
const secondResponse = await session.testAgent.get('/v1/environments/me');

const updatedEchoUrl = secondResponse.body.data.echo.url;

expect(updatedEchoUrl).to.not.equal(oldEchoUrl);
});
});
1 change: 1 addition & 0 deletions apps/api/src/app/environments/environments.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export class EnvironmentsController {
identifier: payload.identifier,
_parentId: payload.parentId,
dns: payload.dns,
bridge: payload.bridge,
})
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { nanoid } from 'nanoid';
import { Injectable } from '@nestjs/common';
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { createHash } from 'crypto';

import { EnvironmentRepository } from '@novu/dal';
import { encryptApiKey } from '@novu/application-generic';
import { ApiException, encryptApiKey, buildBridgeEndpointUrl } from '@novu/application-generic';

import { CreateEnvironmentCommand } from './create-environment.command';
import { GenerateUniqueApiKey } from '../generate-unique-api-key/generate-unique-api-key.usecase';
Expand All @@ -18,7 +19,8 @@ export class CreateEnvironment {
private environmentRepository: EnvironmentRepository,
private createNotificationGroup: CreateNotificationGroup,
private generateUniqueApiKey: GenerateUniqueApiKey,
private createDefaultLayoutUsecase: CreateDefaultLayout
private createDefaultLayoutUsecase: CreateDefaultLayout,
protected moduleRef: ModuleRef
) {}

async execute(command: CreateEnvironmentCommand) {
Expand All @@ -40,6 +42,10 @@ export class CreateEnvironment {
],
});

if (command.name === 'Development') {
await this.storeDefaultTunnelUrl(command.userId, command.organizationId, environment._id, key);
}

if (!command.parentEnvironmentId) {
await this.createNotificationGroup.execute(
CreateNotificationGroupCommand.create({
Expand All @@ -61,4 +67,35 @@ export class CreateEnvironment {

return environment;
}

private async storeDefaultTunnelUrl(userId: string, organizationId: string, environmentId: string, apiKey: string) {
try {
if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') {
if (!require('@novu/ee-echo-api')?.StoreBridgeConfiguration) {
throw new ApiException('Echo api module is not loaded');
}

const baseUrl = process.env.TUNNEL_BASE_ADDRESS;

if (baseUrl === undefined || baseUrl === '') {
throw new InternalServerErrorException('Base tunnel url not configured');
}

const bridgeUrl = buildBridgeEndpointUrl(apiKey, baseUrl);

const usecase = this.moduleRef.get(require('@novu/ee-echo-api')?.StoreBridgeConfiguration, {
strict: false,
});

await usecase.execute({
userId,
organizationId,
environmentId,
bridgeUrl,
});
}
} catch (e) {
Logger.error(e, `Unexpected error while importing enterprise modules`, 'StoreBridgeConfiguration');
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { createHash } from 'crypto';
import { Injectable } from '@nestjs/common';
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';

import { EnvironmentRepository } from '@novu/dal';
import { decryptApiKey, encryptApiKey } from '@novu/application-generic';
import { buildBridgeEndpointUrl, decryptApiKey, encryptApiKey } from '@novu/application-generic';

import { ApiException } from '../../../shared/exceptions/api.exception';
import { GenerateUniqueApiKey } from '../generate-unique-api-key/generate-unique-api-key.usecase';
Expand All @@ -13,7 +14,8 @@ import { IApiKeyDto } from '../../dtos/environment-response.dto';
export class RegenerateApiKeys {
constructor(
private environmentRepository: EnvironmentRepository,
private generateUniqueApiKey: GenerateUniqueApiKey
private generateUniqueApiKey: GenerateUniqueApiKey,
private moduleRef: ModuleRef
) {}

async execute(command: GetApiKeysCommand): Promise<IApiKeyDto[]> {
Expand All @@ -34,11 +36,46 @@ export class RegenerateApiKeys {
hashedApiKey
);

if (environment.name === 'Development') {
this.storeDefaultTunnelUrl(command.userId, command.organizationId, command.environmentId, key);
}

return environments.map((item) => {
return {
_userId: item._userId,
key: decryptApiKey(item.key),
};
});
}

private async storeDefaultTunnelUrl(userId: string, organizationId: string, environmentId: string, apiKey: string) {
try {
if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') {
if (!require('@novu/ee-echo-api')?.StoreBridgeConfiguration) {
throw new ApiException('Echo api module is not loaded');
}

const baseUrl = process.env.TUNNEL_BASE_ADDRESS;

if (baseUrl === undefined || baseUrl === '') {
throw new InternalServerErrorException('Base tunnel url not configured');
}

const bridgeUrl = buildBridgeEndpointUrl(apiKey, baseUrl);

const usecase = this.moduleRef.get(require('@novu/ee-echo-api')?.StoreBridgeConfiguration, {
strict: false,
});

await usecase.execute({
userId,
organizationId,
environmentId,
bridgeUrl,
});
}
} catch (e) {
Logger.error(e, `Unexpected error while importing enterprise modules`, 'StoreBridgeConfiguration');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ export class UpdateEnvironmentCommand extends OrganizationCommand {

@IsOptional()
dns?: { inboundParseDomain?: string };

@IsOptional()
bridge?: { url?: string };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { UserSession } from '@novu/testing';
import { expect } from 'chai';
import { UpdateEnvironmentRequestDto } from '../../dtos/update-environment-request.dto';

describe('Update Environment - /environments (PUT)', async () => {
let session: UserSession;

before(async () => {
session = new UserSession();
await session.initialize();
});

it('should update bridge data correctly', async () => {
const updatePayload: UpdateEnvironmentRequestDto = {
name: 'Development',
bridge: { url: 'http://example.com' },
};

await session.testAgent.put(`/v1/environments/${session.environment._id}`).send(updatePayload).expect(200);
const { body } = await session.testAgent.get('/v1/environments/me');

expect(body.data.name).to.eq(updatePayload.name);
expect(body.data.echo.url).to.equal(updatePayload.bridge?.url);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { EnvironmentEntity, EnvironmentRepository } from '@novu/dal';
import { UserSession } from '@novu/testing';
import { expect } from 'chai';
import { UpdateEnvironmentRequestDto } from '../../dtos/update-environment-request.dto';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@ export class UpdateEnvironment {
updatePayload.identifier = command.identifier;
}

if (command.dns && command.dns.inboundParseDomain !== '') {
if (command.dns && command.dns.inboundParseDomain && command.dns.inboundParseDomain !== '') {
updatePayload[`dns.inboundParseDomain`] = command.dns.inboundParseDomain;
}

if (
(await this.shouldUpdateEchoConfiguration(command)) &&
command.bridge &&
command.bridge.url &&
command.bridge.url !== ''
) {
updatePayload['echo.url'] = command.bridge.url;
}

return await this.environmentRepository.update(
{
_id: command.environmentId,
Expand All @@ -32,4 +41,24 @@ export class UpdateEnvironment {
{ $set: updatePayload }
);
}
async shouldUpdateEchoConfiguration(command: UpdateEnvironmentCommand): Promise<boolean> {
if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') {
let name: string;
if (command.name && command.name !== '') {
name = command.name;
} else {
const env = await this.environmentRepository.findOne({ _id: command.environmentId });

if (!env) {
return false;
}

name = env.name;
}

return name === 'Development';
} else {
return false;
}
}
}
5 changes: 3 additions & 2 deletions apps/api/src/app/subscribers/subscribers.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import { ThrottlerCategory, ThrottlerCost } from '../rate-limiting/guards';
import { MessageMarkAsRequestDto } from '../widgets/dtos/mark-as-request.dto';
import { MarkMessageAsByMarkCommand } from '../widgets/usecases/mark-message-as-by-mark/mark-message-as-by-mark.command';
import { MarkMessageAsByMark } from '../widgets/usecases/mark-message-as-by-mark/mark-message-as-by-mark.usecase';
import { FeedResponseDto } from '../widgets/dtos/feeds-response.dto';

@ThrottlerCategory(ApiRateLimitCategoryEnum.CONFIGURATION)
@ApiCommonResponses()
Expand Down Expand Up @@ -476,12 +477,12 @@ export class SubscribersController {
@ApiOperation({
summary: 'Get in-app notification feed for a particular subscriber',
})
@ApiOkPaginatedResponse(MessageResponseDto)
@ApiOkPaginatedResponse(FeedResponseDto)
async getNotificationsFeed(
@UserSession() user: IJwtPayload,
@Param('subscriberId') subscriberId: string,
@Query() query: GetInAppNotificationsFeedForSubscriberDto
): Promise<PaginatedResponseDto<MessageResponseDto>> {
): Promise<FeedResponseDto> {
let feedsQuery: string[] | undefined;
if (query.feedIdentifier) {
feedsQuery = Array.isArray(query.feedIdentifier) ? query.feedIdentifier : [query.feedIdentifier];
Expand Down
Loading

0 comments on commit 83e55ac

Please sign in to comment.