Skip to content

Commit

Permalink
Merge branch 'next' into feature/add-plain-chat-buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
jainpawan21 committed Dec 4, 2024
2 parents 4764e11 + 17eaa75 commit 9343ebf
Show file tree
Hide file tree
Showing 126 changed files with 6,201 additions and 573 deletions.
5 changes: 4 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,10 @@
"rstrip",
"truncatewords",
"xmlschema",
"jsonify"
"jsonify",
"touchpoint",
"Angularjs",
"navigatable"
],
"flagWords": [],
"patterns": [
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@novu/api",
"version": "2.1.0",
"version": "2.1.1",
"description": "description",
"author": "",
"private": "true",
Expand Down
2 changes: 0 additions & 2 deletions apps/api/src/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ API_RATE_LIMIT_MAXIMUM_UNLIMITED_TRIGGER=
API_RATE_LIMIT_MAXIMUM_UNLIMITED_CONFIGURATION=
API_RATE_LIMIT_MAXIMUM_UNLIMITED_GLOBAL=

PR_PREVIEW_ROOT_URL=dev-web-novu.netlify.app

HUBSPOT_INVITE_NUDGE_EMAIL_USER_LIST_ID=
HUBSPOT_PRIVATE_APP_ACCESS_TOKEN=

Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ modules.push(
client: new Client({
secretKey: process.env.NOVU_INTERNAL_SECRET_KEY,
strictAuthentication:
process.env.NODE_ENV === 'production' || process.env.NOVU_STRICT_AUTHENTICATION_ENABLED === 'true',
process.env.NODE_ENV === 'production' ||
process.env.NODE_ENV === 'dev' ||
process.env.NOVU_STRICT_AUTHENTICATION_ENABLED === 'true',
}),
controllerDecorators: [ApiExcludeController()],
workflows: [usageLimitsWorkflow],
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/app/environments-v1/novu-bridge-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class NovuBridgeClient {

workflows.push(programmaticallyConstructedWorkflow);
}

this.novuRequestHandler = new NovuRequestHandler({
frameworkName,
workflows,
Expand Down
20 changes: 18 additions & 2 deletions apps/api/src/app/inbox/inbox.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Param,
Patch,
Post,
Query,
UseGuards,
Headers,
} from '@nestjs/common';
import { ApiExcludeController } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { SubscriberEntity } from '@novu/dal';
Expand Down Expand Up @@ -51,12 +63,16 @@ export class InboxController {
) {}

@Post('/session')
async sessionInitialize(@Body() body: SubscriberSessionRequestDto): Promise<SubscriberSessionResponseDto> {
async sessionInitialize(
@Body() body: SubscriberSessionRequestDto,
@Headers('origin') origin: string
): Promise<SubscriberSessionResponseDto> {
return await this.initializeSessionUsecase.execute(
SessionCommand.create({
subscriberId: body.subscriberId,
applicationIdentifier: body.applicationIdentifier,
subscriberHash: body.subscriberHash,
origin,
})
);
}
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/app/inbox/usecases/session/session.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ export class SessionCommand extends BaseCommand {
@IsDefined()
@IsString()
readonly subscriberId: string;

@IsOptional()
@IsString()
readonly origin?: string;
}
10 changes: 6 additions & 4 deletions apps/api/src/app/inbox/usecases/session/session.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sinon from 'sinon';
import { expect } from 'chai';
import { NotFoundException } from '@nestjs/common';
import { EnvironmentRepository } from '@novu/dal';
import { EnvironmentRepository, IntegrationRepository } from '@novu/dal';
import { AnalyticsService, CreateSubscriber, SelectIntegration, AuthService } from '@novu/application-generic';
import { ChannelTypeEnum, InAppProviderIdEnum } from '@novu/shared';

Expand Down Expand Up @@ -39,22 +39,24 @@ describe('Session', () => {
let selectIntegration: sinon.SinonStubbedInstance<SelectIntegration>;
let analyticsService: sinon.SinonStubbedInstance<AnalyticsService>;
let notificationsCount: sinon.SinonStubbedInstance<NotificationsCount>;

let integrationRepository: sinon.SinonStubbedInstance<IntegrationRepository>;
beforeEach(() => {
environmentRepository = sinon.createStubInstance(EnvironmentRepository);
createSubscriber = sinon.createStubInstance(CreateSubscriber);
authService = sinon.createStubInstance(AuthService);
selectIntegration = sinon.createStubInstance(SelectIntegration);
analyticsService = sinon.createStubInstance(AnalyticsService);
notificationsCount = sinon.createStubInstance(NotificationsCount);
integrationRepository = sinon.createStubInstance(IntegrationRepository);

session = new Session(
environmentRepository as any,
createSubscriber as any,
authService as any,
selectIntegration as any,
analyticsService as any,
notificationsCount as any
notificationsCount as any,
integrationRepository as any
);
});

Expand Down Expand Up @@ -192,7 +194,7 @@ describe('Session', () => {
expect(response.token).to.equal(token);
expect(response.totalUnreadCount).to.equal(notificationCount.data[0].count);
expect(
analyticsService.mixpanelTrack.calledOnceWith(AnalyticsEventsEnum.SESSION_INITIALIZED, '', {
analyticsService.mixpanelTrack.calledWith(AnalyticsEventsEnum.SESSION_INITIALIZED, '', {
_organization: environment._organizationId,
environmentName: environment.name,
_subscriber: subscriber._id,
Expand Down
34 changes: 32 additions & 2 deletions apps/api/src/app/inbox/usecases/session/session.usecase.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { EnvironmentRepository } from '@novu/dal';
import { EnvironmentRepository, IntegrationRepository } from '@novu/dal';
import { ChannelTypeEnum, InAppProviderIdEnum } from '@novu/shared';
import {
AnalyticsService,
Expand Down Expand Up @@ -27,7 +27,8 @@ export class Session {
private authService: AuthService,
private selectIntegration: SelectIntegration,
private analyticsService: AnalyticsService,
private notificationsCount: NotificationsCount
private notificationsCount: NotificationsCount,
private integrationRepository: IntegrationRepository
) {}

@LogDecorator()
Expand Down Expand Up @@ -88,6 +89,35 @@ export class Session {

const removeNovuBranding = inAppIntegration.removeNovuBranding || false;

/**
* We want to prevent the playground inbox demo from marking the integration as connected
* And only treat the real customer domain or local environment as valid origins
*/
const isOriginFromNovu =
command.origin &&
((process.env.DASHBOARD_V2_BASE_URL && command.origin?.includes(process.env.DASHBOARD_V2_BASE_URL as string)) ||
(process.env.FRONT_BASE_URL && command.origin?.includes(process.env.FRONT_BASE_URL as string)));

if (!isOriginFromNovu && !inAppIntegration.connected) {
this.analyticsService.mixpanelTrack(AnalyticsEventsEnum.INBOX_CONNECTED, '', {
_organization: environment._organizationId,
environmentName: environment.name,
});

await this.integrationRepository.updateOne(
{
_id: inAppIntegration._id,
_organizationId: environment._organizationId,
_environmentId: environment._id,
},
{
$set: {
connected: true,
},
}
);
}

return {
token,
totalUnreadCount,
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/app/inbox/utils/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum AnalyticsEventsEnum {
SESSION_INITIALIZED = 'Session Initialized - [Inbox]',
INBOX_CONNECTED = 'Inbox Connected - [Inbox]',
FETCH_NOTIFICATIONS = 'Fetch Notifications - [Inbox]',
MARK_NOTIFICATION_AS = 'Mark Notification As - [Inbox]',
UPDATE_NOTIFICATION_ACTION = 'Update Notification Action - [Inbox]',
Expand Down
69 changes: 15 additions & 54 deletions apps/api/src/config/cors.config.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spy } from 'sinon';
import { expect } from 'chai';
import { corsOptionsDelegate, isPermittedDeployPreviewOrigin } from './cors.config';
import { corsOptionsDelegate } from './cors.config';

describe('CORS Configuration', () => {
describe('Local Environment', () => {
Expand Down Expand Up @@ -32,7 +32,6 @@ describe('CORS Configuration', () => {
process.env.FRONT_BASE_URL = 'https://test.com';
process.env.LEGACY_STAGING_DASHBOARD_URL = 'https://test-legacy-staging-dashboard.com';
process.env.WIDGET_BASE_URL = 'https://widget.com';
process.env.PR_PREVIEW_ROOT_URL = 'https://pr-preview.com';
});

afterEach(() => {
Expand All @@ -43,14 +42,26 @@ describe('CORS Configuration', () => {
const callbackSpy = spy();

// @ts-expect-error - corsOptionsDelegate is not typed correctly
corsOptionsDelegate({ url: '/v1/test' }, callbackSpy);
corsOptionsDelegate(
{
url: '/v1/test',
headers: {
origin: 'https://test.novu.com',
},
},
callbackSpy
);

expect(callbackSpy.calledOnce).to.be.ok;
expect(callbackSpy.firstCall.firstArg).to.be.null;
expect(callbackSpy.firstCall.lastArg.origin.length).to.equal(3);
expect(callbackSpy.firstCall.lastArg.origin.length).to.equal(environment === 'dev' ? 4 : 3);
expect(callbackSpy.firstCall.lastArg.origin[0]).to.equal(process.env.FRONT_BASE_URL);
expect(callbackSpy.firstCall.lastArg.origin[1]).to.equal(process.env.LEGACY_STAGING_DASHBOARD_URL);
expect(callbackSpy.firstCall.lastArg.origin[2]).to.equal(process.env.WIDGET_BASE_URL);

if (environment === 'dev') {
expect(callbackSpy.firstCall.lastArg.origin[3]).to.equal('https://test.novu.com');
}
});

it('widget routes should be wildcarded', () => {
Expand All @@ -74,56 +85,6 @@ describe('CORS Configuration', () => {
expect(callbackSpy.firstCall.firstArg).to.be.null;
expect(callbackSpy.firstCall.lastArg.origin).to.equal('*');
});

if (environment === 'dev') {
it('should allow all origins for dev environment from pr preview', () => {
const callbackSpy = spy();

// @ts-expect-error - corsOptionsDelegate is not typed correctly
corsOptionsDelegate(
{
url: '/v1/test',
headers: {
origin: `https://test--${process.env.PR_PREVIEW_ROOT_URL}`,
},
},
callbackSpy
);

expect(callbackSpy.calledOnce).to.be.ok;
expect(callbackSpy.firstCall.firstArg).to.be.null;
expect(callbackSpy.firstCall.lastArg.origin).to.equal('*');
});
}
});
});

describe('isPermittedDeployPreviewOrigin', () => {
afterEach(() => {
process.env.NODE_ENV = 'test';
});

it('should return false when NODE_ENV is not dev', () => {
process.env.NODE_ENV = 'production';
expect(isPermittedDeployPreviewOrigin('https://someorigin.com')).to.be.false;
});

it('should return false when PR_PREVIEW_ROOT_URL is not set', () => {
process.env.NODE_ENV = 'dev';
delete process.env.PR_PREVIEW_ROOT_URL;
expect(isPermittedDeployPreviewOrigin('https://someorigin.com')).to.be.false;
});

it('should return false for origins not matching PR_PREVIEW_ROOT_URL (string)', () => {
process.env.NODE_ENV = 'dev';
process.env.PR_PREVIEW_ROOT_URL = 'https://pr-preview.com';
expect(isPermittedDeployPreviewOrigin('https://anotherorigin.com')).to.be.false;
});

it('should return true for origin matching PR_PREVIEW_ROOT_URL', () => {
process.env.NODE_ENV = 'dev';
process.env.PR_PREVIEW_ROOT_URL = 'https://pr-preview.com';
expect(isPermittedDeployPreviewOrigin('https://netlify-https://pr-preview.com')).to.be.true;
});
});
});
32 changes: 6 additions & 26 deletions apps/api/src/config/cors.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ export const corsOptionsDelegate: Parameters<INestApplication['enableCors']>[0]
methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
};

const origin = extractOrigin(req);

if (enableWildcard(req)) {
corsOptions.origin = '*';
} else {
Expand All @@ -29,27 +27,17 @@ export const corsOptionsDelegate: Parameters<INestApplication['enableCors']>[0]
if (process.env.WIDGET_BASE_URL) {
corsOptions.origin.push(process.env.WIDGET_BASE_URL);
}
// Enable preview deployments in staging environment for Netlify and Vercel
if (process.env.NODE_ENV === 'dev') {
corsOptions.origin.push(origin(req));
}
}

const shouldDisableCorsForPreviewUrls = isPermittedDeployPreviewOrigin(origin);

Logger.verbose(`Should allow deploy preview? ${shouldDisableCorsForPreviewUrls ? 'Yes' : 'No'}.`, {
curEnv: process.env.NODE_ENV,
previewUrlRoot: process.env.PR_PREVIEW_ROOT_URL,
origin,
});

callback(null as unknown as Error, corsOptions);
};

function enableWildcard(req: Request): boolean {
return (
isSandboxEnvironment() ||
isWidgetRoute(req.url) ||
isInboxRoute(req.url) ||
isBlueprintRoute(req.url) ||
isPermittedDeployPreviewOrigin(extractOrigin(req))
);
return isSandboxEnvironment() || isWidgetRoute(req.url) || isInboxRoute(req.url) || isBlueprintRoute(req.url);
}

function isWidgetRoute(url: string): boolean {
Expand All @@ -68,14 +56,6 @@ function isSandboxEnvironment(): boolean {
return ['test', 'local'].includes(process.env.NODE_ENV);
}

export function isPermittedDeployPreviewOrigin(origin: string | string[]): boolean {
if (!process.env.PR_PREVIEW_ROOT_URL || process.env.NODE_ENV !== 'dev') {
return false;
}

return origin.includes(process.env.PR_PREVIEW_ROOT_URL);
}

function extractOrigin(req: Request): string {
function origin(req: Request): string {
return (req.headers as any)?.origin || '';
}
5 changes: 4 additions & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@
"@segment/analytics-next": "^1.73.0",
"@sentry/react": "^8.35.0",
"@tanstack/react-query": "^5.59.6",
"launchdarkly-react-client-sdk": "^3.3.2",
"@types/js-cookie": "^3.0.6",
"@uiw/codemirror-extensions-langs": "^4.23.6",
"@uiw/codemirror-theme-material": "^4.23.6",
"@uiw/codemirror-theme-white": "^4.23.6",
"@uiw/codemirror-themes": "^4.23.6",
"@uiw/react-codemirror": "^4.23.6",
Expand All @@ -67,12 +67,15 @@
"flat": "^6.0.1",
"motion": "^11.12.0",
"js-cookie": "^3.0.5",
"launchdarkly-react-client-sdk": "^3.3.2",
"lodash.debounce": "^4.0.8",
"lodash.merge": "^4.6.2",
"lucide-react": "^0.439.0",
"mixpanel-browser": "^2.52.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-confetti": "^6.1.0",
"react-dom": "^18.3.1",
"react-helmet-async": "^1.3.0",
"react-hook-form": "7.53.2",
Expand Down
Loading

0 comments on commit 9343ebf

Please sign in to comment.