diff --git a/apps/api/src/app/inbox/dtos/inbox-channel-connection-response.dto.ts b/apps/api/src/app/inbox/dtos/inbox-channel-connection-response.dto.ts new file mode 100644 index 00000000000..9e271560b96 --- /dev/null +++ b/apps/api/src/app/inbox/dtos/inbox-channel-connection-response.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class InboxChannelConnectionResponseDto { + @ApiProperty({ + description: 'The unique identifier of the channel connection.', + type: String, + }) + identifier: string; +} + +export class InboxListChannelConnectionsResponseDto { + @ApiProperty({ type: [InboxChannelConnectionResponseDto] }) + data: InboxChannelConnectionResponseDto[]; + + @ApiProperty({ type: String, nullable: true }) + next: string | null; + + @ApiProperty({ type: String, nullable: true }) + previous: string | null; +} diff --git a/apps/api/src/app/inbox/dtos/inbox-channel-endpoint-response.dto.ts b/apps/api/src/app/inbox/dtos/inbox-channel-endpoint-response.dto.ts new file mode 100644 index 00000000000..7547995f9a2 --- /dev/null +++ b/apps/api/src/app/inbox/dtos/inbox-channel-endpoint-response.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ChannelEndpointType, ENDPOINT_TYPES } from '@novu/shared'; + +export class InboxChannelEndpointResponseDto { + @ApiProperty({ + description: 'The unique identifier of the channel endpoint.', + type: String, + }) + identifier: string; + + @ApiProperty({ + description: 'Type of channel endpoint', + enum: Object.values(ENDPOINT_TYPES), + example: ENDPOINT_TYPES.SLACK_CHANNEL, + }) + type: ChannelEndpointType; +} + +export class InboxListChannelEndpointsResponseDto { + @ApiProperty({ type: [InboxChannelEndpointResponseDto] }) + data: InboxChannelEndpointResponseDto[]; + + @ApiProperty({ type: String, nullable: true }) + next: string | null; + + @ApiProperty({ type: String, nullable: true }) + previous: string | null; +} diff --git a/apps/api/src/app/inbox/dtos/inbox-dto.mapper.ts b/apps/api/src/app/inbox/dtos/inbox-dto.mapper.ts new file mode 100644 index 00000000000..b8b0480cbde --- /dev/null +++ b/apps/api/src/app/inbox/dtos/inbox-dto.mapper.ts @@ -0,0 +1,16 @@ +import { ChannelConnectionEntity, ChannelEndpointEntity } from '@novu/dal'; +import { InboxChannelConnectionResponseDto } from './inbox-channel-connection-response.dto'; +import { InboxChannelEndpointResponseDto } from './inbox-channel-endpoint-response.dto'; + +export function mapChannelConnectionToInboxDto(entity: ChannelConnectionEntity): InboxChannelConnectionResponseDto { + return { + identifier: entity.identifier, + }; +} + +export function mapChannelEndpointToInboxDto(entity: ChannelEndpointEntity): InboxChannelEndpointResponseDto { + return { + identifier: entity.identifier, + type: entity.type, + }; +} diff --git a/apps/api/src/app/inbox/inbox.controller.ts b/apps/api/src/app/inbox/inbox.controller.ts index 52aa05dd29a..e47ba5e28c0 100644 --- a/apps/api/src/app/inbox/inbox.controller.ts +++ b/apps/api/src/app/inbox/inbox.controller.ts @@ -26,26 +26,14 @@ import { TriggerRequestCategoryEnum, UserSessionData, } from '@novu/shared'; -import { CreateChannelConnectionRequestDto } from '../channel-connections/dtos/create-channel-connection-request.dto'; -import { mapChannelConnectionEntityToDto } from '../channel-connections/dtos/dto.mapper'; -import { GetChannelConnectionResponseDto } from '../channel-connections/dtos/get-channel-connection-response.dto'; import { ListChannelConnectionsQueryDto } from '../channel-connections/dtos/list-channel-connections-query.dto'; -import { ListChannelConnectionsResponseDto } from '../channel-connections/dtos/list-channel-connections-response.dto'; -import { CreateChannelConnectionCommand } from '../channel-connections/usecases/create-channel-connection/create-channel-connection.command'; -import { CreateChannelConnection } from '../channel-connections/usecases/create-channel-connection/create-channel-connection.usecase'; import { DeleteChannelConnectionCommand } from '../channel-connections/usecases/delete-channel-connection/delete-channel-connection.command'; import { DeleteChannelConnection } from '../channel-connections/usecases/delete-channel-connection/delete-channel-connection.usecase'; import { GetChannelConnectionCommand } from '../channel-connections/usecases/get-channel-connection/get-channel-connection.command'; import { GetChannelConnection } from '../channel-connections/usecases/get-channel-connection/get-channel-connection.usecase'; import { ListChannelConnectionsCommand } from '../channel-connections/usecases/list-channel-connections/list-channel-connections.command'; import { ListChannelConnections } from '../channel-connections/usecases/list-channel-connections/list-channel-connections.usecase'; -import { CreateChannelEndpointRequest } from '../channel-endpoints/dtos/create-channel-endpoint-request.dto'; -import { mapChannelEndpointEntityToDto } from '../channel-endpoints/dtos/dto.mapper'; -import { GetChannelEndpointResponseDto } from '../channel-endpoints/dtos/get-channel-endpoint-response.dto'; import { ListChannelEndpointsQueryDto } from '../channel-endpoints/dtos/list-channel-endpoints-query.dto'; -import { ListChannelEndpointsResponseDto } from '../channel-endpoints/dtos/list-channel-endpoints-response.dto'; -import { CreateChannelEndpointCommand } from '../channel-endpoints/usecases/create-channel-endpoint/create-channel-endpoint.command'; -import { CreateChannelEndpoint } from '../channel-endpoints/usecases/create-channel-endpoint/create-channel-endpoint.usecase'; import { DeleteChannelEndpointCommand } from '../channel-endpoints/usecases/delete-channel-endpoint/delete-channel-endpoint.command'; import { DeleteChannelEndpoint } from '../channel-endpoints/usecases/delete-channel-endpoint/delete-channel-endpoint.usecase'; import { GetChannelEndpointCommand } from '../channel-endpoints/usecases/get-channel-endpoint/get-channel-endpoint.command'; @@ -77,6 +65,12 @@ import { GetNotificationsRequestDto } from './dtos/get-notifications-request.dto import { GetNotificationsResponseDto } from './dtos/get-notifications-response.dto'; import { GetPreferencesRequestDto } from './dtos/get-preferences-request.dto'; import { GetPreferencesResponseDto } from './dtos/get-preferences-response.dto'; +import { + InboxChannelConnectionResponseDto, + InboxListChannelConnectionsResponseDto, +} from './dtos/inbox-channel-connection-response.dto'; +import { InboxListChannelEndpointsResponseDto } from './dtos/inbox-channel-endpoint-response.dto'; +import { mapChannelConnectionToInboxDto, mapChannelEndpointToInboxDto } from './dtos/inbox-dto.mapper'; import { InboxNotificationDto } from './dtos/inbox-notification.dto'; import { MarkNotificationsAsSeenRequestDto } from './dtos/mark-notifications-as-seen-request.dto'; import { SnoozeNotificationRequestDto } from './dtos/snooze-notification-request.dto'; @@ -139,11 +133,9 @@ export class InboxController { private deleteAllNotificationsUsecase: DeleteAllNotifications, private listChannelConnectionsUsecase: ListChannelConnections, private getChannelConnectionUsecase: GetChannelConnection, - private createChannelConnectionUsecase: CreateChannelConnection, private deleteChannelConnectionUsecase: DeleteChannelConnection, private listChannelEndpointsUsecase: ListChannelEndpoints, private getChannelEndpointUsecase: GetChannelEndpoint, - private createChannelEndpointUsecase: CreateChannelEndpoint, private deleteChannelEndpointUsecase: DeleteChannelEndpoint, private generateChatOauthUrlUsecase: GenerateChatOauthUrl, private featureFlagsService: FeatureFlagsService @@ -674,7 +666,7 @@ export class InboxController { async listChannelConnections( @SubscriberSession() subscriberSession: SubscriberSession, @Query() query: ListChannelConnectionsQueryDto - ): Promise { + ): Promise { await this.checkChannelFeatureEnabled(subscriberSession._organizationId); const result = await this.listChannelConnectionsUsecase.execute( @@ -698,11 +690,9 @@ export class InboxController { ); return { - data: result.data.map(mapChannelConnectionEntityToDto), - next: result.next, - previous: result.previous, - totalCount: result.totalCount!, - totalCountCapped: result.totalCountCapped!, + data: result.data.map(mapChannelConnectionToInboxDto), + next: result.next ?? null, + previous: result.previous ?? null, }; } @@ -711,7 +701,7 @@ export class InboxController { async getChannelConnection( @SubscriberSession() subscriberSession: SubscriberSession, @Param('identifier') identifier: string - ): Promise { + ): Promise { await this.checkChannelFeatureEnabled(subscriberSession._organizationId); const channelConnection = await this.getChannelConnectionUsecase.execute( @@ -726,31 +716,7 @@ export class InboxController { throw new NotFoundException(`Channel connection not found: ${identifier}`); } - return mapChannelConnectionEntityToDto(channelConnection); - } - - @UseGuards(AuthGuard('subscriberJwt')) - @Post('/channel-connections') - async createChannelConnection( - @SubscriberSession() subscriberSession: SubscriberSession, - @Body() body: CreateChannelConnectionRequestDto - ): Promise { - await this.checkChannelFeatureEnabled(subscriberSession._organizationId); - - const channelConnection = await this.createChannelConnectionUsecase.execute( - CreateChannelConnectionCommand.create({ - environmentId: subscriberSession._environmentId, - organizationId: subscriberSession._organizationId, - identifier: body.identifier, - integrationIdentifier: body.integrationIdentifier, - subscriberId: subscriberSession.subscriberId, - context: body.context, - workspace: body.workspace, - auth: body.auth, - }) - ); - - return mapChannelConnectionEntityToDto(channelConnection); + return mapChannelConnectionToInboxDto(channelConnection); } @UseGuards(AuthGuard('subscriberJwt')) @@ -788,7 +754,7 @@ export class InboxController { async listChannelEndpoints( @SubscriberSession() subscriberSession: SubscriberSession, @Query() query: ListChannelEndpointsQueryDto - ): Promise { + ): Promise { await this.checkChannelFeatureEnabled(subscriberSession._organizationId); const result = await this.listChannelEndpointsUsecase.execute( @@ -813,66 +779,12 @@ export class InboxController { ); return { - data: result.data.map(mapChannelEndpointEntityToDto), - next: result.next, - previous: result.previous, - totalCount: result.totalCount!, - totalCountCapped: result.totalCountCapped!, + data: result.data.map(mapChannelEndpointToInboxDto), + next: result.next ?? null, + previous: result.previous ?? null, }; } - @UseGuards(AuthGuard('subscriberJwt')) - @Get('/channel-endpoints/:identifier') - async getChannelEndpoint( - @SubscriberSession() subscriberSession: SubscriberSession, - @Param('identifier') identifier: string - ): Promise { - await this.checkChannelFeatureEnabled(subscriberSession._organizationId); - - const channelEndpoint = await this.getChannelEndpointUsecase.execute( - GetChannelEndpointCommand.create({ - environmentId: subscriberSession._environmentId, - organizationId: subscriberSession._organizationId, - identifier, - }) - ); - - if (channelEndpoint.subscriberId && channelEndpoint.subscriberId !== subscriberSession.subscriberId) { - throw new NotFoundException(`Channel endpoint not found: ${identifier}`); - } - - return mapChannelEndpointEntityToDto(channelEndpoint); - } - - @UseGuards(AuthGuard('subscriberJwt')) - @Post('/channel-endpoints') - async createChannelEndpoint( - @SubscriberSession() subscriberSession: SubscriberSession, - @Body() body: CreateChannelEndpointRequest - ): Promise { - await this.checkChannelFeatureEnabled(subscriberSession._organizationId); - - // Cast needed because CreateChannelEndpointRequest is a discriminated union; the type/endpoint - // fields are correctly validated by class-validator before reaching this handler. - const typedBody = body as Extract; - - const channelEndpoint = await this.createChannelEndpointUsecase.execute( - CreateChannelEndpointCommand.create({ - environmentId: subscriberSession._environmentId, - organizationId: subscriberSession._organizationId, - identifier: typedBody.identifier, - integrationIdentifier: typedBody.integrationIdentifier, - connectionIdentifier: typedBody.connectionIdentifier, - subscriberId: subscriberSession.subscriberId, - context: typedBody.context, - type: typedBody.type, - endpoint: typedBody.endpoint, - } as Parameters[0]) - ); - - return mapChannelEndpointEntityToDto(channelEndpoint); - } - @UseGuards(AuthGuard('subscriberJwt')) @Delete('/channel-endpoints/:identifier') @HttpCode(HttpStatus.NO_CONTENT) diff --git a/apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts b/apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts index d01e5b9fb97..4643ba3b226 100644 --- a/apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts +++ b/apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts @@ -62,18 +62,16 @@ export class GenerateSlackOauthUrl { await this.assertResourceExists(command); const { clientId } = await this.getIntegrationCredentials(command.integration); - const subscriberId = command.connectionMode === 'shared' ? undefined : command.subscriberId; const secureState = await this.createSecureState( command.integration, - subscriberId, + command.subscriberId, command.context, command.connectionIdentifier, command.mode, command.connectionMode ); - const resolvedScope = - command.mode === 'link_user' ? undefined : await this.resolveBotScopes(command); + const resolvedScope = command.mode === 'link_user' ? undefined : await this.resolveBotScopes(command); return this.getOAuthUrl(clientId!, secureState, resolvedScope, command.userScope, command.mode); } diff --git a/packages/js/src/channel-connections/types.ts b/packages/js/src/channel-connections/types.ts index e4b89bd16a0..fb1aa591a93 100644 --- a/packages/js/src/channel-connections/types.ts +++ b/packages/js/src/channel-connections/types.ts @@ -2,28 +2,11 @@ import type { Context } from '../types'; export type ChannelConnectionResponse = { identifier: string; - integrationIdentifier: string; - providerId: string; - channel: string; - subscriberId?: string; - contextKeys: string[]; - workspace: { id: string; name?: string }; - createdAt: string; - updatedAt: string; }; export type ChannelEndpointResponse = { identifier: string; - integrationIdentifier: string; - connectionIdentifier?: string; - providerId: string; - channel: string; - subscriberId: string; - contextKeys: string[]; type: string; - endpoint: Record; - createdAt: string; - updatedAt: string; }; export type OAuthMode = 'connect' | 'link_user';