Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(root): include local tunnel work #5698

Merged
merged 22 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
10dfc49
feat: include local tunnel work
denis-kralj-novu Jun 7, 2024
b20a0ed
feat: include local tunnel work
denis-kralj-novu Jun 7, 2024
0420589
feat: include local tunnel work
denis-kralj-novu Jun 9, 2024
7bad5cd
feat: include local tunnel work
denis-kralj-novu Jun 9, 2024
0244ab6
feat: include local tunnel work
denis-kralj-novu Jun 9, 2024
d503dab
feat: include local tunnel work
denis-kralj-novu Jun 10, 2024
655c795
feat: include local tunnel work
denis-kralj-novu Jun 10, 2024
677377c
feat: include local tunnel work
denis-kralj-novu Jun 10, 2024
3f62384
feat: include local tunnel work
denis-kralj-novu Jun 10, 2024
90c774c
feat: include local tunnel work
denis-kralj-novu Jun 10, 2024
71ad0f2
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
f1b951b
Merge branch 'next' into include-local-tunnel
denis-kralj-novu Jun 11, 2024
3fdff04
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
45a3d2d
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
4f42449
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
9ec063d
Merge branch 'next' into include-local-tunnel
denis-kralj-novu Jun 11, 2024
cd0d119
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
7e8600b
Merge branch 'next' into include-local-tunnel
denis-kralj-novu Jun 11, 2024
4792379
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
afe74ea
Merge branch 'next' into include-local-tunnel
denis-kralj-novu Jun 11, 2024
3285b63
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
f8c1d84
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
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 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: 'Testy.test-org@gmail.com',
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 !== '') {
rifont marked this conversation as resolved.
Show resolved Hide resolved
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') {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I initially thought that I could use the command that I defined in the ee package for updating the endpoint. However, this would then lead to a situation where that command would return the results of the update, wheres the previous set of adjustments in the caller would have its own result. I could do consolidation logic since it is aiming via environmentId, but that seemed a bit messy. Let me know if that would still be preferable to this way of updating the data.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rifont let me hear your thoughts on this as well

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is a suitable approach for the time being 👍

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;
}
}
}
1 change: 1 addition & 0 deletions libs/application-generic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from './utils/hmac';
export * from './utils/novu-integrations';
export * from './utils/require-inject';
export * from './utils/variants';
export * from './utils/buildBridgeEndpointUrl';
export * from './decorators';
export * from './tracing';
export * from './dtos';
Expand Down
22 changes: 22 additions & 0 deletions libs/application-generic/src/utils/buildBridgeEndpointUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createHash } from 'crypto';

/*
* Creates a bridge endpoint url to be used for request from novu cloud to the local
* workflow definition
*/
export const buildBridgeEndpointUrl = (
apiKey: string,
baseAddress: string
): string => {
return `${buildBridgeSubdomain(apiKey)}.${baseAddress}`;
};

/*
* Creates a bridge subdomain based on the apiKey provided. This function is used in several
* places, including packages/create-novu-app/templates/index.ts when generating the
* subdomain in the bridge application. Developers should take care to keep changes
* in sync.
*/
Comment on lines +15 to +19
Copy link
Collaborator

Choose a reason for hiding this comment

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

👏 praise: ‏Nice comment 🙏

export const buildBridgeSubdomain = (apiKey: string): string => {
return createHash('md5').update(apiKey).digest('hex');
};
denis-kralj-novu marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading