feat: secure and user-scope metadata resolvers for messaging infrastructure (#18787)

## Summary

Builds on the messaging infrastructure migration (#18784) by securing
and user-scoping all 4 metadata resolvers:

### DTOs secured
- **ConnectedAccountDTO**: `@HideField()` on `accessToken`,
`refreshToken`, `connectionParameters`, `oidcTokenClaims`
- **MessageChannelDTO / CalendarChannelDTO**: `@HideField()` on
`syncCursor`
- **MessageFolderDTO**: `@HideField()` on `syncCursor`, `externalId`
- **UpdateMessageFolderInputUpdates**: stripped to only `isSynced`
(removed `name`, `syncCursor`, `pendingSyncAction`)

### Resolvers user-scoped via `@AuthUserWorkspaceId()`
- `myConnectedAccounts` — returns only the calling user's accounts (no
permission guard)
- `myMessageChannels(connectedAccountId?)` — returns channels for the
user's connected accounts
- `myCalendarChannels(connectedAccountId?)` — same pattern
- `myMessageFolders(messageChannelId?)` — returns folders through the
ownership chain

### Admin-only listing with permission guard
- `connectedAccounts` query retained with
`SettingsPermissionGuard(CONNECTED_ACCOUNTS)` for admin listing of all
workspace accounts

### Unsafe mutations removed
- Removed `createConnectedAccount`, `updateConnectedAccount` (OAuth/IMAP
flows create/refresh tokens server-side)
- Removed `create*`/`delete*` mutations from MessageChannel,
CalendarChannel, MessageFolder (managed by sync engine)

### Update mutations restricted with ownership verification
- `deleteConnectedAccount(id)` — verifies `entity.userWorkspaceId ===
currentUserWorkspaceId`
- `updateMessageChannel` / `updateCalendarChannel` /
`updateMessageFolder` — verify ownership through connected account chain
- New `OWNERSHIP_VIOLATION` exception codes map to `ForbiddenError` in
GraphQL

### `@AuthUserWorkspaceId` decorator hardened
- Added `allowUndefined` option (default: `false`) — throws
`ForbiddenException` if `userWorkspaceId` is undefined (e.g. API key
auth)
- Existing callers updated to `@AuthUserWorkspaceId({ allowUndefined:
true })` where needed
- New user-scoped resolvers enforce non-undefined `userWorkspaceId` at
decorator level

### Exception handler chaining
- `MessageFolderGraphqlApiExceptionInterceptor`,
`MessageChannelGraphqlApiExceptionInterceptor`,
`CalendarChannelGraphqlApiExceptionInterceptor` chain upstream exception
handling (ConnectedAccountException, MessageChannelException) for
correct `ForbiddenError` propagation

### Metadata services enhanced
- `findByUserWorkspaceId()`, `getUserConnectedAccountIds()`,
`findByConnectedAccountIds()`, `findByMessageChannelIds()`
- `findBy*ForUser()` methods encapsulate ownership checks before
querying
- `verifyOwnership()` on all 4 services with proper chain validation
- Named parameters throughout for clarity

### Dev seeds for both schemas
- Added JANE to connected account, message channel, calendar channel
workspace seeds
- Created message folder workspace seeds (TIM, JONY, JANE)
- New `seed-metadata-entities.util.ts` seeds core schema tables
(connectedAccount, messageChannel, calendarChannel, messageFolder) with
same IDs as workspace seeds, mapping `accountOwnerId` →
`userWorkspaceId`

### Integration tests (using seeds, not raw SQL)
- 4 test suites (`connected-account`, `message-channel`,
`calendar-channel`, `message-folder`)
- Tests use seeded data IDs from seed constants — no raw SQL
inserts/deletes
- Tests read via GraphQL resolvers
- Tests cover: user scoping, admin permission checks, sensitive field
exclusion, ownership enforcement on mutations

### Frontend migration
- Feature-flag-gated hooks (`useMyConnectedAccounts`,
`useMyMessageChannels`, `useMyCalendarChannels`, `useMyMessageFolders`)
- When `IS_CONNECTED_ACCOUNT_MIGRATED` is on: hooks use metadata API
(`POST /metadata`)
- When flag is off: hooks use existing workspace API (`POST /graphql`,
current behavior)
- Settings account pages updated to use new hooks
- `useEffect` extracted to
`SettingsAccountsSelectedMessageChannelEffect` component per project
conventions
- Error messages translated with Lingui

## Test plan
- [x] Server typecheck passes
- [x] Server lint passes
- [x] Server unit tests pass (477 suites, 4269 tests)
- [x] Frontend typecheck passes
- [x] Frontend lint passes
- [x] Integration tests verify user-scoping, ownership enforcement,
hidden fields
- [ ] CI green

---------

Co-authored-by: neo773 <neo773@protonmail.com>
This commit is contained in:
Charles Bochet
2026-03-20 17:22:22 +01:00
committed by GitHub
parent a8625d8bfb
commit 9cb21e71fa
88 changed files with 3412 additions and 1233 deletions
@@ -96,10 +96,55 @@ export class MessageChannelDataAccessService {
)
: await this.toCoreWhere(workspaceId, where);
return this.coreRepository.findOne({
const requestedRelations =
(options.relations as string[] | undefined)?.slice() ?? [];
const needsConnectedAccount =
requestedRelations.includes('connectedAccount');
const needsMessageFolders = requestedRelations.includes('messageFolders');
const coreRelations = requestedRelations.filter(
(r) => r !== 'connectedAccount' && r !== 'messageFolders',
);
const result = await this.coreRepository.findOne({
...options,
where: coreWhere,
} as FindOneOptions<MessageChannelEntity>) as unknown as Promise<MessageChannelWorkspaceEntity | null>;
relations: coreRelations,
} as FindOneOptions<MessageChannelEntity>);
if (!result) {
return null;
}
const workspaceResult =
result as unknown as MessageChannelWorkspaceEntity;
if (needsConnectedAccount) {
const connectedAccount =
await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { id: result.connectedAccountId },
});
if (connectedAccount) {
workspaceResult.connectedAccount = connectedAccount;
}
}
if (needsMessageFolders) {
const workspaceRepository =
await this.getWorkspaceRepository(workspaceId);
const workspaceChannel = await workspaceRepository.findOne({
where: { id: result.id },
relations: ['messageFolders'],
});
workspaceResult.messageFolders = workspaceChannel?.messageFolders ?? [];
}
return workspaceResult;
}
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
@@ -75,9 +75,7 @@ export class MessageChannelDTO {
@Field()
isSyncEnabled: boolean;
@IsString()
@IsOptional()
@Field(() => String, { nullable: true })
@HideField()
syncCursor: string | null;
@IsDateString()
@@ -46,13 +46,14 @@ export class MessageChannelEntity extends WorkspaceRelatedEntity {
})
type: MessageChannelType;
@Column({ type: 'boolean', nullable: false })
@Column({ type: 'boolean', nullable: false, default: true })
isContactAutoCreationEnabled: boolean;
@Column({
type: 'enum',
enum: MessageChannelContactAutoCreationPolicy,
nullable: false,
default: MessageChannelContactAutoCreationPolicy.SENT,
})
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
@@ -60,13 +61,14 @@ export class MessageChannelEntity extends WorkspaceRelatedEntity {
type: 'enum',
enum: MessageFolderImportPolicy,
nullable: false,
default: MessageFolderImportPolicy.ALL_FOLDERS,
})
messageFolderImportPolicy: MessageFolderImportPolicy;
@Column({ type: 'boolean', nullable: false })
@Column({ type: 'boolean', nullable: false, default: true })
excludeNonProfessionalEmails: boolean;
@Column({ type: 'boolean', nullable: false })
@Column({ type: 'boolean', nullable: false, default: true })
excludeGroupEmails: boolean;
@Column({
@@ -76,7 +78,7 @@ export class MessageChannelEntity extends WorkspaceRelatedEntity {
})
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
@Column({ type: 'boolean', nullable: false })
@Column({ type: 'boolean', nullable: false, default: true })
isSyncEnabled: boolean;
@Column({ type: 'varchar', nullable: true })
@@ -3,11 +3,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor';
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
import { MessageChannelResolver } from 'src/engine/metadata-modules/message-channel/resolvers/message-channel.resolver';
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
@Module({
imports: [
@@ -15,6 +18,9 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
AuthModule,
PermissionsModule,
FeatureFlagModule,
ConnectedAccountMetadataModule,
MessageFolderDataAccessModule,
MessagingImportManagerModule,
],
providers: [
MessageChannelMetadataService,
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { In, Repository } from 'typeorm';
import {
MessageChannelSyncStage,
@@ -9,36 +9,137 @@ import {
MessageChannelVisibility,
} from 'twenty-shared/types';
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import {
MessageChannelException,
MessageChannelExceptionCode,
} from 'src/engine/metadata-modules/message-channel/message-channel.exception';
@Injectable()
export class MessageChannelMetadataService {
constructor(
@InjectRepository(MessageChannelEntity)
private readonly repository: Repository<MessageChannelEntity>,
private readonly connectedAccountMetadataService: ConnectedAccountMetadataService,
) {}
async findAll(workspaceId: string): Promise<MessageChannelDTO[]> {
return this.repository.find({ where: { workspaceId } });
}
async findByConnectedAccountId(
connectedAccountId: string,
workspaceId: string,
): Promise<MessageChannelDTO[]> {
async findByUserWorkspaceId({
userWorkspaceId,
workspaceId,
}: {
userWorkspaceId: string;
workspaceId: string;
}): Promise<MessageChannelDTO[]> {
const userAccountIds =
await this.connectedAccountMetadataService.getUserConnectedAccountIds({
userWorkspaceId,
workspaceId,
});
return this.findByConnectedAccountIds({
connectedAccountIds: userAccountIds,
workspaceId,
});
}
async findByConnectedAccountIdForUser({
connectedAccountId,
userWorkspaceId,
workspaceId,
}: {
connectedAccountId: string;
userWorkspaceId: string;
workspaceId: string;
}): Promise<MessageChannelDTO[]> {
await this.connectedAccountMetadataService.verifyOwnership({
id: connectedAccountId,
userWorkspaceId,
workspaceId,
});
return this.findByConnectedAccountId({ connectedAccountId, workspaceId });
}
async findByConnectedAccountId({
connectedAccountId,
workspaceId,
}: {
connectedAccountId: string;
workspaceId: string;
}): Promise<MessageChannelDTO[]> {
return this.repository.find({
where: { connectedAccountId, workspaceId },
});
}
async findById(
id: string,
workspaceId: string,
): Promise<MessageChannelDTO | null> {
async findByConnectedAccountIds({
connectedAccountIds,
workspaceId,
}: {
connectedAccountIds: string[];
workspaceId: string;
}): Promise<MessageChannelDTO[]> {
if (connectedAccountIds.length === 0) {
return [];
}
return this.repository.find({
where: { connectedAccountId: In(connectedAccountIds), workspaceId },
});
}
async findById({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<MessageChannelDTO | null> {
return this.repository.findOne({ where: { id, workspaceId } });
}
async verifyOwnership({
id,
userWorkspaceId,
workspaceId,
}: {
id: string;
userWorkspaceId: string;
workspaceId: string;
}): Promise<MessageChannelEntity> {
const messageChannel = await this.repository.findOne({
where: { id, workspaceId },
});
if (!messageChannel) {
throw new MessageChannelException(
`Message channel ${id} not found`,
MessageChannelExceptionCode.MESSAGE_CHANNEL_NOT_FOUND,
);
}
const userAccountIds =
await this.connectedAccountMetadataService.getUserConnectedAccountIds({
userWorkspaceId,
workspaceId,
});
if (!userAccountIds.includes(messageChannel.connectedAccountId)) {
throw new MessageChannelException(
`Message channel ${id} does not belong to user workspace ${userWorkspaceId}`,
MessageChannelExceptionCode.MESSAGE_CHANNEL_OWNERSHIP_VIOLATION,
);
}
return messageChannel;
}
async create(
data: Partial<MessageChannelEntity> & {
workspaceId: string;
@@ -54,11 +155,15 @@ export class MessageChannelMetadataService {
return this.repository.save(entity);
}
async update(
id: string,
workspaceId: string,
data: Partial<MessageChannelEntity>,
): Promise<MessageChannelDTO> {
async update({
id,
workspaceId,
data,
}: {
id: string;
workspaceId: string;
data: Partial<MessageChannelEntity>;
}): Promise<MessageChannelDTO> {
await this.repository.update(
{ id, workspaceId },
data as Record<string, unknown>,
@@ -67,13 +172,19 @@ export class MessageChannelMetadataService {
return this.repository.findOneOrFail({ where: { id, workspaceId } });
}
async delete(id: string, workspaceId: string): Promise<MessageChannelDTO> {
const entity = await this.repository.findOneOrFail({
async delete({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<MessageChannelDTO> {
const messageChannel = await this.repository.findOneOrFail({
where: { id, workspaceId },
});
await this.repository.delete({ id, workspaceId });
return entity;
return messageChannel;
}
}
@@ -7,6 +7,7 @@ import { CustomException } from 'src/utils/custom-exception';
export enum MessageChannelExceptionCode {
MESSAGE_CHANNEL_NOT_FOUND = 'MESSAGE_CHANNEL_NOT_FOUND',
INVALID_MESSAGE_CHANNEL_INPUT = 'INVALID_MESSAGE_CHANNEL_INPUT',
MESSAGE_CHANNEL_OWNERSHIP_VIOLATION = 'MESSAGE_CHANNEL_OWNERSHIP_VIOLATION',
}
const getMessageChannelExceptionUserFriendlyMessage = (
@@ -17,6 +18,8 @@ const getMessageChannelExceptionUserFriendlyMessage = (
return msg`Message channel not found.`;
case MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT:
return msg`Invalid message channel input.`;
case MessageChannelExceptionCode.MESSAGE_CHANNEL_OWNERSHIP_VIOLATION:
return msg`You do not have access to this message channel.`;
default:
assertUnreachable(code);
}
@@ -1,24 +1,37 @@
import { UseGuards, UseInterceptors } from '@nestjs/common';
import { Args, Mutation, Query } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import {
FeatureFlagGuard,
RequireFeatureFlag,
} from 'src/engine/guards/feature-flag.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateMessageChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/create-message-channel.input';
import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
import { UpdateMessageChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/update-message-channel.input';
import {
MessageChannelException,
MessageChannelExceptionCode,
} from 'src/engine/metadata-modules/message-channel/message-channel.exception';
import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor';
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
import {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
import { Not } from 'typeorm';
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
@UseInterceptors(MessageChannelGraphqlApiExceptionInterceptor)
@@ -26,13 +39,16 @@ import { MessageChannelMetadataService } from 'src/engine/metadata-modules/messa
export class MessageChannelResolver {
constructor(
private readonly messageChannelMetadataService: MessageChannelMetadataService,
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService,
) {}
@Query(() => [MessageChannelDTO])
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
@UseGuards(NoPermissionGuard)
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
async messageChannels(
async myMessageChannels(
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUserWorkspaceId() userWorkspaceId: string,
@Args('connectedAccountId', {
type: () => UUIDScalarType,
nullable: true,
@@ -40,59 +56,80 @@ export class MessageChannelResolver {
connectedAccountId?: string,
): Promise<MessageChannelDTO[]> {
if (connectedAccountId) {
return this.messageChannelMetadataService.findByConnectedAccountId(
connectedAccountId,
workspace.id,
return this.messageChannelMetadataService.findByConnectedAccountIdForUser(
{
connectedAccountId,
userWorkspaceId,
workspaceId: workspace.id,
},
);
}
return this.messageChannelMetadataService.findAll(workspace.id);
}
@Query(() => MessageChannelDTO, { nullable: true })
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
async messageChannel(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<MessageChannelDTO | null> {
return this.messageChannelMetadataService.findById(id, workspace.id);
}
@Mutation(() => MessageChannelDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
async createMessageChannel(
@Args('input') input: CreateMessageChannelInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<MessageChannelDTO> {
return this.messageChannelMetadataService.create({
...input,
return this.messageChannelMetadataService.findByUserWorkspaceId({
userWorkspaceId,
workspaceId: workspace.id,
});
}
@Mutation(() => MessageChannelDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
@UseGuards(NoPermissionGuard)
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
async updateMessageChannel(
@Args('input') input: UpdateMessageChannelInput,
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUserWorkspaceId() userWorkspaceId: string,
): Promise<MessageChannelDTO> {
return this.messageChannelMetadataService.update(
input.id,
workspace.id,
input.update,
);
}
const messageChannel =
await this.messageChannelMetadataService.verifyOwnership({
id: input.id,
userWorkspaceId,
workspaceId: workspace.id,
});
@Mutation(() => MessageChannelDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
async deleteMessageChannel(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<MessageChannelDTO> {
return this.messageChannelMetadataService.delete(id, workspace.id);
const isSyncOngoing =
messageChannel.syncStage ===
MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING;
const foldersWithPendingAction =
await this.messageFolderDataAccessService.find(workspace.id, {
messageChannelId: messageChannel.id,
pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE),
});
const hasPendingGroupEmailsAction =
messageChannel.pendingGroupEmailsAction !==
MessageChannelPendingGroupEmailsAction.NONE;
if (
isSyncOngoing &&
(foldersWithPendingAction.length > 0 || hasPendingGroupEmailsAction)
) {
throw new MessageChannelException(
'Cannot update message channel while sync is ongoing with pending actions',
MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT,
);
}
if (
messageChannel.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION &&
isDefined(input.update.excludeGroupEmails) &&
input.update.excludeGroupEmails !== messageChannel.excludeGroupEmails
) {
// Service expects WorkspaceEntity type but only reads .id
await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction(
messageChannel as unknown as MessageChannelWorkspaceEntity,
workspace.id,
input.update.excludeGroupEmails
? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION
: MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT,
);
}
return this.messageChannelMetadataService.update({
id: input.id,
workspaceId: workspace.id,
data: input.update,
});
}
}
@@ -1,9 +1,14 @@
import { assertUnreachable } from 'twenty-shared/utils';
import {
ForbiddenError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
ConnectedAccountException,
ConnectedAccountExceptionCode,
} from 'src/engine/metadata-modules/connected-account/connected-account.exception';
import {
MessageChannelException,
MessageChannelExceptionCode,
@@ -16,11 +21,23 @@ export const messageChannelGraphqlApiExceptionHandler = (error: Error) => {
throw new NotFoundError(error);
case MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT:
throw new UserInputError(error);
case MessageChannelExceptionCode.MESSAGE_CHANNEL_OWNERSHIP_VIOLATION:
throw new ForbiddenError(error);
default: {
return assertUnreachable(error.code);
}
}
}
if (error instanceof ConnectedAccountException) {
switch (error.code) {
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION:
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND:
throw new ForbiddenError(error);
default:
break;
}
}
throw error;
};