Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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

This file was deleted.

204 changes: 202 additions & 2 deletions apps/api/src/app/integrations/integrations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
GetDecryptedIntegrations,
IntegrationResponseDto,
OtelSpan,
PinoLogger,
RequirePermissions,
} from '@novu/application-generic';
import { CommunityOrganizationRepository } from '@novu/dal';
Expand Down Expand Up @@ -55,23 +56,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,8 +113,16 @@ 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,
private logger: PinoLogger
) {
this.logger.setContext(IntegrationsController.name);
}

@Get('/')
@ApiOkResponse({
Expand Down Expand Up @@ -412,6 +433,185 @@ 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_WRITE)
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,
})
);
}

/**
* 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 };
}

/**
* 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) {
this.logger.error({ err }, 'Azure OAuth callback failed');

res
.status(200)
.type('html')
.send(
AzureSetupOauthCallback.buildPopupHtml({
success: false,
errorMessage: 'An unexpected error occurred while completing Azure setup.',
})
);
}
}

/**
* @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