Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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: 1 addition & 1 deletion apps/api/src/app/agents/agents-webhook.controller.ts
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

by this direct return the nest js filter were not triggered and that caused missing logs for the 500 errors

Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class AgentsWebhookController {
if (err instanceof HttpException) {
res.status(err.getStatus()).json(err.getResponse());
} else {
res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ error: 'Internal server error' });
throw err;
}
}
}
Expand Down
193 changes: 192 additions & 1 deletion apps/api/src/app/integrations/integrations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,35 @@ import { ChannelTypeLimitDto } from './dtos/get-channel-type-limit.sto';
import { UpdateIntegrationRequestDto } from './dtos/update-integration.dto';
import { AutoConfigureIntegrationCommand } from './usecases/auto-configure-integration/auto-configure-integration.command';
import { AutoConfigureIntegration } from './usecases/auto-configure-integration/auto-configure-integration.usecase';
import { AzureSetupOauthCallbackCommand } from './usecases/azure-setup-oauth-callback/azure-setup-oauth-callback.command';
import { AzureSetupOauthCallback } from './usecases/azure-setup-oauth-callback/azure-setup-oauth-callback.usecase';
import { ChatOauthCallbackCommand } from './usecases/chat-oauth-callback/chat-oauth-callback.command';
import { ResponseTypeEnum } from './usecases/chat-oauth-callback/chat-oauth-callback.response';
import { ChatOauthCallback } from './usecases/chat-oauth-callback/chat-oauth-callback.usecase';
import { CreateIntegrationCommand } from './usecases/create-integration/create-integration.command';
import { CreateIntegration } from './usecases/create-integration/create-integration.usecase';
import { GenerateAzureSetupOauthUrlCommand } from './usecases/generate-azure-setup-oauth-url/generate-azure-setup-oauth-url.command';
import { GenerateAzureSetupOauthUrl } from './usecases/generate-azure-setup-oauth-url/generate-azure-setup-oauth-url.usecase';
import { GenerateChatOauthUrlCommand } from './usecases/generate-chat-oath-url/generate-chat-oauth-url.command';
import { GenerateChatOauthUrl } from './usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase';
import { GenerateConnectOauthUrlCommand } from './usecases/generate-chat-oath-url/generate-connect-oauth-url.command';
import { GenerateConnectOauthUrl } from './usecases/generate-chat-oath-url/generate-connect-oauth-url.usecase';
import { GenerateLinkUserOauthUrlCommand } from './usecases/generate-chat-oath-url/generate-link-user-oauth-url.command';
import { GenerateLinkUserOauthUrl } from './usecases/generate-chat-oath-url/generate-link-user-oauth-url.usecase';
import { GenerateMsTeamsArmTemplateCommand } from './usecases/generate-msteams-arm-template/generate-msteams-arm-template.command';
import { GenerateMsTeamsArmTemplate } from './usecases/generate-msteams-arm-template/generate-msteams-arm-template.usecase';
import { GetMsTeamsArmTemplate } from './usecases/generate-msteams-arm-template/get-msteams-arm-template.usecase';
import { GetInAppActivatedCommand } from './usecases/get-in-app-activated/get-in-app-activated.command';
import { GetInAppActivated } from './usecases/get-in-app-activated/get-in-app-activated.usecase';
import { GetIntegrationsCommand } from './usecases/get-integrations/get-integrations.command';
import { GetIntegrations } from './usecases/get-integrations/get-integrations.usecase';
import { GetWebhookSupportStatusCommand } from './usecases/get-webhook-support-status/get-webhook-support-status.command';
import { GetWebhookSupportStatus } from './usecases/get-webhook-support-status/get-webhook-support-status.usecase';
import { MsTeamsHealthCheckCommand } from './usecases/msteams-health-check/msteams-health-check.command';
import {
MsTeamsHealthCheck,
MsTeamsHealthCheckResult,
} from './usecases/msteams-health-check/msteams-health-check.usecase';
import { RemoveIntegrationCommand } from './usecases/remove-integration/remove-integration.command';
import { RemoveIntegration } from './usecases/remove-integration/remove-integration.usecase';
import { SetIntegrationAsPrimaryCommand } from './usecases/set-integration-as-primary/set-integration-as-primary.command';
Expand Down Expand Up @@ -100,7 +112,12 @@ export class IntegrationsController {
private generateConnectOauthUrlUsecase: GenerateConnectOauthUrl,
private generateLinkUserOauthUrlUsecase: GenerateLinkUserOauthUrl,
private chatOauthCallbackUsecase: ChatOauthCallback,
private featureFlagsService: FeatureFlagsService
private featureFlagsService: FeatureFlagsService,
private generateMsTeamsArmTemplateUsecase: GenerateMsTeamsArmTemplate,
private getMsTeamsArmTemplateUsecase: GetMsTeamsArmTemplate,
private generateAzureSetupOauthUrlUsecase: GenerateAzureSetupOauthUrl,
private azureSetupOauthCallbackUsecase: AzureSetupOauthCallback,
private msTeamsHealthCheckUsecase: MsTeamsHealthCheck
) {}

@Get('/')
Expand Down Expand Up @@ -412,6 +429,180 @@ export class IntegrationsController {
);
}

@Get('/:integrationId/msteams-arm-template/deploy-url')
@ApiOkResponse({
description: 'Signed Azure Portal "Deploy to Azure" URL for the MS Teams ARM template.',
})
@ApiOperation({
summary: 'Get MS Teams ARM template deploy URL',
description:
'Returns a short-lived signed URL that opens the Azure Portal with a pre-filled ARM template to create the Azure Bot resource and enable the MS Teams channel.',
})
@ApiExcludeEndpoint()
@RequireAuthentication()
@RequirePermissions(PermissionsEnum.INTEGRATION_READ)
async getMsTeamsArmTemplateDeployUrl(
@UserSession() user: UserSessionData,
@Param('integrationId') integrationId: string
): Promise<{ deployUrl: string }> {
return this.generateMsTeamsArmTemplateUsecase.execute(
GenerateMsTeamsArmTemplateCommand.create({
userId: user._id,
organizationId: user.organizationId,
integrationId,
})
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Public endpoint fetched by Azure Portal when the user clicks "Deploy to Azure".
* Protected by an HMAC-signed, time-expiring `sig` + `exp` query parameter pair —
* no session cookie is available because Azure's servers make this request, not the browser.
*/
@Get('/:integrationId/msteams-arm-template')
@ApiExcludeEndpoint()
@ApiOperation({ summary: 'Serve MS Teams ARM template JSON (signed)' })
async getMsTeamsArmTemplateJson(
@Res() res: Response,
@Param('integrationId') integrationId: string,
@Query('sig') sig: string,
@Query('exp') exp: string
): Promise<void> {
if (!sig || !exp) {
throw new BadRequestException('Missing required parameters: sig, exp');
}

const { template } = await this.getMsTeamsArmTemplateUsecase.execute(integrationId, sig, exp);

res.setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'no-store');
res.send(JSON.stringify(template, null, 2));
}

/**
* Quick Setup: generate an Azure AD OAuth URL so Novu can create the App Registration
* on the user's behalf via Microsoft Graph.
*/
@Get('/:integrationId/msteams-azure-setup/oauth-url')
@ApiOkResponse({
description: 'Azure AD OAuth URL for the Quick Setup flow (Novu creates the app registration).',
})
@ApiOperation({
summary: 'Get Azure Quick Setup OAuth URL',
description:
'Returns an Azure AD OAuth URL that authorizes Novu to create an App Registration and client secret on your behalf via Microsoft Graph.',
})
@ApiExcludeEndpoint()
@RequireAuthentication()
@RequirePermissions(PermissionsEnum.INTEGRATION_WRITE)
async getAzureSetupOauthUrl(
@UserSession() user: UserSessionData,
@Param('integrationId') integrationId: string
): Promise<{ url: string }> {
const url = await this.generateAzureSetupOauthUrlUsecase.execute(
GenerateAzureSetupOauthUrlCommand.create({
userId: user._id,
organizationId: user.organizationId,
environmentId: user.environmentId,
integrationId,
})
);

return { url };
}
Comment on lines +490 to +516
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate the Azure Quick Setup entrypoint with the rollout flag.

The dashboard hides Quick Setup behind IS_MSTEAMS_QUICK_SETUP_ENABLED, but this endpoint still issues a usable signed OAuth URL to any caller with integration-write access. That makes the rollout flag UI-only.

💡 Suggested fix
   async getAzureSetupOauthUrl(
     `@UserSession`() user: UserSessionData,
     `@Param`('integrationId') integrationId: string
   ): Promise<{ url: string }> {
+    const isEnabled = await this.featureFlagsService.getFlag({
+      key: FeatureFlagsKeysEnum.IS_MSTEAMS_QUICK_SETUP_ENABLED,
+      defaultValue: false,
+      organization: { _id: user.organizationId },
+    });
+
+    if (!isEnabled) {
+      throw new NotFoundException('Feature not enabled');
+    }
+
     const url = await this.generateAzureSetupOauthUrlUsecase.execute(
       GenerateAzureSetupOauthUrlCommand.create({
         userId: user._id,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Get('/:integrationId/msteams-azure-setup/oauth-url')
@ApiOkResponse({
description: 'Azure AD OAuth URL for the Quick Setup flow (Novu creates the app registration).',
})
@ApiOperation({
summary: 'Get Azure Quick Setup OAuth URL',
description:
'Returns an Azure AD OAuth URL that authorizes Novu to create an App Registration and client secret on your behalf via Microsoft Graph.',
})
@ApiExcludeEndpoint()
@RequireAuthentication()
@RequirePermissions(PermissionsEnum.INTEGRATION_WRITE)
async getAzureSetupOauthUrl(
@UserSession() user: UserSessionData,
@Param('integrationId') integrationId: string
): Promise<{ url: string }> {
const url = await this.generateAzureSetupOauthUrlUsecase.execute(
GenerateAzureSetupOauthUrlCommand.create({
userId: user._id,
organizationId: user.organizationId,
environmentId: user.environmentId,
integrationId,
})
);
return { url };
}
`@Get`('/:integrationId/msteams-azure-setup/oauth-url')
`@ApiOkResponse`({
description: 'Azure AD OAuth URL for the Quick Setup flow (Novu creates the app registration).',
})
`@ApiOperation`({
summary: 'Get Azure Quick Setup OAuth URL',
description:
'Returns an Azure AD OAuth URL that authorizes Novu to create an App Registration and client secret on your behalf via Microsoft Graph.',
})
`@ApiExcludeEndpoint`()
`@RequireAuthentication`()
`@RequirePermissions`(PermissionsEnum.INTEGRATION_WRITE)
async getAzureSetupOauthUrl(
`@UserSession`() user: UserSessionData,
`@Param`('integrationId') integrationId: string
): Promise<{ url: string }> {
const isEnabled = await this.featureFlagsService.getFlag({
key: FeatureFlagsKeysEnum.IS_MSTEAMS_QUICK_SETUP_ENABLED,
defaultValue: false,
organization: { _id: user.organizationId },
});
if (!isEnabled) {
throw new NotFoundException('Feature not enabled');
}
const url = await this.generateAzureSetupOauthUrlUsecase.execute(
GenerateAzureSetupOauthUrlCommand.create({
userId: user._id,
organizationId: user.organizationId,
environmentId: user.environmentId,
integrationId,
})
);
return { url };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/integrations/integrations.controller.ts` around lines 487 -
513, The getAzureSetupOauthUrl controller currently always returns a signed
OAuth URL; gate this endpoint with the same rollout flag used in the UI by
checking the feature flag (IS_MSTEAMS_QUICK_SETUP_ENABLED) at the start of the
getAzureSetupOauthUrl method and reject requests when it is disabled (e.g.,
throw a 403/404 or use existing feature-flag guard logic). Modify
getAzureSetupOauthUrl to read the flag (via your feature-flag/service or
environment helper) and short-circuit before calling
generateAzureSetupOauthUrlUsecase.execute when the flag is false, returning the
appropriate error response so the endpoint behavior matches the UI rollout.
Ensure you reference the existing method getAzureSetupOauthUrl and the
constant/name IS_MSTEAMS_QUICK_SETUP_ENABLED when implementing the check.


/**
* Health-check endpoint polled by the dashboard to determine if the saved MS Teams
* credentials, app catalog entry, and Graph permissions are ready after the Quick
* Setup OAuth flow.
*/
@Get('/:integrationId/msteams-health')
@ApiOkResponse({
description: 'Per-checkpoint health status for an MS Teams integration after Quick Setup.',
})
@ApiOperation({
summary: 'Get MS Teams integration health status',
description:
'Returns the readiness status of the stored MS Teams credentials, app catalog entry, and Graph permissions. Poll this endpoint after the OAuth setup completes to determine when it is safe to proceed to admin consent.',
})
@ApiExcludeEndpoint()
@RequireAuthentication()
@RequirePermissions(PermissionsEnum.INTEGRATION_READ)
async getMsTeamsHealth(
@UserSession() user: UserSessionData,
@Param('integrationId') integrationId: string,
@Query('checks') checksParam?: string
): Promise<MsTeamsHealthCheckResult> {
const checks = checksParam
? checksParam
.split(',')
.map((s) => s.trim())
.filter(Boolean)
: undefined;

return this.msTeamsHealthCheckUsecase.execute(
MsTeamsHealthCheckCommand.create({
environmentId: user.environmentId,
organizationId: user.organizationId,
integrationId,
checks,
})
);
}

/**
* Quick Setup callback: Azure AD redirects here after the user authorizes Novu.
* Creates the App Registration, secret, and service principal via Graph, saves
* credentials to the integration, then attempts to upload the Teams app to the catalog.
* Returns a self-closing script that posts a message to the opener tab and closes itself.
*/
@Get('/chat/oauth/azure-setup/callback')
@ApiExcludeEndpoint()
@ApiOperation({ summary: 'Azure Quick Setup OAuth callback' })
async handleAzureSetupOauthCallback(
@Res() res: Response,
@Query('code') code?: string,
@Query('state') state?: string,
@Query('error') error?: string,
@Query('error_description') errorDescription?: string
): Promise<void> {
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'");

if (!state) {
res
.status(400)
.type('html')
.send(
AzureSetupOauthCallback.buildPopupHtml({
success: false,
errorMessage: 'Missing required OAuth parameter: state',
})
);

return;
}

try {
const result = await this.azureSetupOauthCallbackUsecase.execute(
AzureSetupOauthCallbackCommand.create({
state,
code,
error,
errorDescription,
})
);

res.type('html').send(result.html);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred during Azure setup';

res
.status(200)
.type('html')
.send(AzureSetupOauthCallback.buildPopupHtml({ success: false, errorMessage }));
Comment thread
djabarovgeorge marked this conversation as resolved.
Outdated
}
}

/**
* @deprecated Use POST /integrations/channel-connections/oauth or POST /integrations/channel-endpoints/oauth instead.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { BaseCommand } from '@novu/application-generic';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';

export class AzureSetupOauthCallbackCommand extends BaseCommand {
@IsNotEmpty()
@IsString()
readonly state: string;

@IsOptional()
@IsString()
readonly code?: string;

@IsOptional()
@IsString()
readonly error?: string;

@IsOptional()
@IsString()
readonly errorDescription?: string;
}
Loading
Loading