Ej/fix message visibility (#11874)
<img width="257" alt="Screenshot 2025-05-05 at 15 30 09" src="https://github.com/user-attachments/assets/5a8e18e0-efc5-4521-9c3a-bf73277ecdf9" /> <img width="257" alt="Screenshot 2025-05-05 at 15 29 05" src="https://github.com/user-attachments/assets/c1a784af-a744-497a-b6ce-ec3a9e8b851a" /> <img width="257" alt="Screenshot 2025-05-05 at 15 33 06" src="https://github.com/user-attachments/assets/c5fabd1d-a125-49d7-aade-0a208a0eff95" /> related to PR https://github.com/twentyhq/twenty/pull/11840 and issue https://github.com/twentyhq/twenty/issues/9826
This commit is contained in:
@ -1,11 +1,12 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
|
||||
import { EventCardMessageNotShared } from '@/activities/timeline-activities/rows/message/components/EventCardMessageNotShared';
|
||||
import { EventCardMessageBodyNotShared } from '@/activities/timeline-activities/rows/message/components/EventCardMessageBodyNotShared';
|
||||
import { EventCardMessageForbidden } from '@/activities/timeline-activities/rows/message/components/EventCardMessageForbidden';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
|
||||
@ -85,7 +86,7 @@ export const EventCardMessage = ({
|
||||
);
|
||||
|
||||
if (shouldHideMessageContent) {
|
||||
return <EventCardMessageNotShared sharedByFullName={authorFullName} />;
|
||||
return <EventCardMessageForbidden notSharedByFullName={authorFullName} />;
|
||||
}
|
||||
|
||||
const shouldHandleNotFound = error.graphQLErrors.some(
|
||||
@ -99,7 +100,7 @@ export const EventCardMessage = ({
|
||||
return <div>Error loading message</div>;
|
||||
}
|
||||
|
||||
if (loading || isUndefined(message)) {
|
||||
if (loading || !isDefined(message)) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
@ -112,12 +113,21 @@ export const EventCardMessage = ({
|
||||
<StyledEventCardMessageContainer>
|
||||
<StyledEmailContent>
|
||||
<StyledEmailTop>
|
||||
<StyledEmailTitle>{message.subject}</StyledEmailTitle>
|
||||
<StyledEmailTitle>
|
||||
{message.subject !==
|
||||
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
|
||||
? message.subject
|
||||
: 'Subject not shared'}
|
||||
</StyledEmailTitle>
|
||||
<StyledEmailParticipants>
|
||||
<OverflowingTextWithTooltip text={messageParticipantHandles} />
|
||||
</StyledEmailParticipants>
|
||||
</StyledEmailTop>
|
||||
<StyledEmailBody>{message.text}</StyledEmailBody>
|
||||
{message.text !== FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED ? (
|
||||
<StyledEmailBody>{message.text}</StyledEmailBody>
|
||||
) : (
|
||||
<EventCardMessageBodyNotShared notSharedByFullName={authorFullName} />
|
||||
)}
|
||||
</StyledEmailContent>
|
||||
</StyledEventCardMessageContainer>
|
||||
);
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { IconLock } from 'twenty-ui/display';
|
||||
|
||||
const StyledEmailBodyNotSharedContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
|
||||
height: 80px;
|
||||
justify-content: center;
|
||||
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledEmailBodyNotSharedIconContainer = styled.div`
|
||||
display: flex;
|
||||
width: ${({ theme }) => theme.icon.size.sm}px;
|
||||
height: ${({ theme }) => theme.icon.size.sm}px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledEmailBodyNotShared = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const EventCardMessageBodyNotShared = ({
|
||||
notSharedByFullName,
|
||||
}: {
|
||||
notSharedByFullName: string;
|
||||
}) => {
|
||||
return (
|
||||
<StyledEmailBodyNotSharedContainer>
|
||||
<StyledEmailBodyNotShared>
|
||||
<StyledEmailBodyNotSharedIconContainer>
|
||||
<IconLock />
|
||||
</StyledEmailBodyNotSharedIconContainer>
|
||||
<span>Not shared by {notSharedByFullName}</span>
|
||||
</StyledEmailBodyNotShared>
|
||||
</StyledEmailBodyNotSharedContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import { EventCardMessageBodyNotShared } from '@/activities/timeline-activities/rows/message/components/EventCardMessageBodyNotShared';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledEventCardMessageContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledEmailContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledEmailTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const EventCardMessageForbidden = ({
|
||||
notSharedByFullName,
|
||||
}: {
|
||||
notSharedByFullName: string;
|
||||
}) => {
|
||||
return (
|
||||
<StyledEventCardMessageContainer>
|
||||
<StyledEmailContent>
|
||||
<StyledEmailTitle>
|
||||
<span>Subject not shared</span>
|
||||
</StyledEmailTitle>
|
||||
<EventCardMessageBodyNotShared
|
||||
notSharedByFullName={notSharedByFullName}
|
||||
/>
|
||||
</StyledEmailContent>
|
||||
</StyledEventCardMessageContainer>
|
||||
);
|
||||
};
|
||||
@ -1,86 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { IconLock } from 'twenty-ui/display';
|
||||
|
||||
const StyledEventCardMessageContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledEmailContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledEmailTop = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledEmailTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledEmailBodyNotShareContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
|
||||
height: 80px;
|
||||
justify-content: center;
|
||||
padding: 0 ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledEmailBodyNotSharedIconContainer = styled.div`
|
||||
display: flex;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledEmailBodyNotShare = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: 0 ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const EventCardMessageNotShared = ({
|
||||
sharedByFullName,
|
||||
}: {
|
||||
sharedByFullName: string;
|
||||
}) => {
|
||||
return (
|
||||
<StyledEventCardMessageContainer>
|
||||
<StyledEmailContent>
|
||||
<StyledEmailTop>
|
||||
<StyledEmailTitle>
|
||||
<span>Subject not shared</span>
|
||||
</StyledEmailTitle>
|
||||
</StyledEmailTop>
|
||||
<StyledEmailBodyNotShareContainer>
|
||||
<StyledEmailBodyNotShare>
|
||||
<StyledEmailBodyNotSharedIconContainer>
|
||||
<IconLock />
|
||||
</StyledEmailBodyNotSharedIconContainer>
|
||||
<span>Not shared by {sharedByFullName}</span>
|
||||
</StyledEmailBodyNotShare>
|
||||
</StyledEmailBodyNotShareContainer>
|
||||
</StyledEmailContent>
|
||||
</StyledEventCardMessageContainer>
|
||||
);
|
||||
};
|
||||
@ -222,7 +222,7 @@ export class TimelineMessagingService {
|
||||
|
||||
const visibilityValues = Object.values(MessageChannelVisibility);
|
||||
|
||||
const threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants:
|
||||
const threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotOwner:
|
||||
| {
|
||||
[key: string]: MessageChannelVisibility;
|
||||
}
|
||||
@ -247,10 +247,10 @@ export class TimelineMessagingService {
|
||||
const threadVisibilityByThreadId: {
|
||||
[key: string]: MessageChannelVisibility;
|
||||
} = messageThreadIds.reduce((threadVisibilityAcc, messageThreadId) => {
|
||||
// If the workspace member is not in the participants of the thread, use the visibility value from the query
|
||||
// If the workspace member is not the owner of the thread, use the visibility value from the query
|
||||
threadVisibilityAcc[messageThreadId] =
|
||||
threadIdsWithoutWorkspaceMember.includes(messageThreadId)
|
||||
? (threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants?.[
|
||||
? (threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotOwner?.[
|
||||
messageThreadId
|
||||
] ?? MessageChannelVisibility.METADATA)
|
||||
: MessageChannelVisibility.SHARE_EVERYTHING;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
|
||||
import { TimelineThread } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
|
||||
import { extractParticipantSummary } from 'src/engine/core-modules/messaging/utils/extract-participant-summary.util';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
@ -19,10 +21,23 @@ export const formatThreads = (
|
||||
[key: string]: MessageChannelVisibility;
|
||||
},
|
||||
): TimelineThread[] => {
|
||||
return threads.map((thread) => ({
|
||||
...thread,
|
||||
...extractParticipantSummary(threadParticipantsByThreadId[thread.id]),
|
||||
visibility: threadVisibilityByThreadId[thread.id],
|
||||
read: true,
|
||||
}));
|
||||
return threads.map((thread) => {
|
||||
const visibility = threadVisibilityByThreadId[thread.id];
|
||||
|
||||
return {
|
||||
...thread,
|
||||
subject:
|
||||
visibility === MessageChannelVisibility.SHARE_EVERYTHING ||
|
||||
visibility === MessageChannelVisibility.SUBJECT
|
||||
? thread.subject
|
||||
: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||
lastMessageBody:
|
||||
visibility === MessageChannelVisibility.SHARE_EVERYTHING
|
||||
? thread.lastMessageBody
|
||||
: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||
...extractParticipantSummary(threadParticipantsByThreadId[thread.id]),
|
||||
visibility,
|
||||
read: true,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
import { BadRequestException, NotFoundException, Scope } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { CanAccessCalendarEventsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-events.service';
|
||||
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||
|
||||
@WorkspaceQueryHook({
|
||||
key: `calendarEvent.findMany`,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class CalendarEventFindManyPreQueryHook
|
||||
implements WorkspaceQueryHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
private readonly canAccessCalendarEventsService: CanAccessCalendarEventsService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
objectName: string,
|
||||
payload: FindManyResolverArgs,
|
||||
): Promise<FindManyResolverArgs> {
|
||||
if (!payload?.filter?.id?.eq) {
|
||||
throw new BadRequestException('id filter is required');
|
||||
}
|
||||
|
||||
if (!authContext.user?.id) {
|
||||
throw new BadRequestException('User id is required');
|
||||
}
|
||||
|
||||
const calendarChannelEventAssociationRepository =
|
||||
await this.twentyORMManager.getRepository<CalendarChannelEventAssociationWorkspaceEntity>(
|
||||
'calendarChannelEventAssociation',
|
||||
);
|
||||
|
||||
const calendarChannelCalendarEventAssociations =
|
||||
await calendarChannelEventAssociationRepository.find({
|
||||
where: {
|
||||
calendarEventId: payload?.filter?.id?.eq,
|
||||
},
|
||||
relations: ['calendarChannel.connectedAccount'],
|
||||
});
|
||||
|
||||
if (calendarChannelCalendarEventAssociations.length === 0) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await this.canAccessCalendarEventsService.canAccessCalendarEvents(
|
||||
authContext.user.id,
|
||||
authContext.workspace.id,
|
||||
calendarChannelCalendarEventAssociations,
|
||||
);
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
import { BadRequestException, NotFoundException, Scope } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { CanAccessCalendarEventsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-events.service';
|
||||
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||
|
||||
@WorkspaceQueryHook({
|
||||
key: `calendarEvent.findOne`,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class CalendarEventFindOnePreQueryHook
|
||||
implements WorkspaceQueryHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
private readonly canAccessCalendarEventsService: CanAccessCalendarEventsService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
objectName: string,
|
||||
payload: FindOneResolverArgs,
|
||||
): Promise<FindOneResolverArgs> {
|
||||
if (!payload?.filter?.id?.eq) {
|
||||
throw new BadRequestException('id filter is required');
|
||||
}
|
||||
|
||||
if (!authContext.user?.id) {
|
||||
throw new BadRequestException('User id is required');
|
||||
}
|
||||
|
||||
const calendarChannelEventAssociationRepository =
|
||||
await this.twentyORMManager.getRepository<CalendarChannelEventAssociationWorkspaceEntity>(
|
||||
'calendarChannelEventAssociation',
|
||||
);
|
||||
|
||||
const calendarChannelCalendarEventAssociations =
|
||||
await calendarChannelEventAssociationRepository.find({
|
||||
where: {
|
||||
calendarEventId: payload?.filter?.id?.eq,
|
||||
},
|
||||
relations: ['calendarChannel', 'calendarChannel.connectedAccount'],
|
||||
});
|
||||
|
||||
if (calendarChannelCalendarEventAssociations.length === 0) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await this.canAccessCalendarEventsService.canAccessCalendarEvents(
|
||||
authContext.user.id,
|
||||
authContext.workspace.id,
|
||||
calendarChannelCalendarEventAssociations,
|
||||
);
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@ -28,7 +28,7 @@ export class ApplyCalendarEventsVisibilityRestrictionsService {
|
||||
where: {
|
||||
calendarEventId: In(calendarEvents.map((event) => event.id)),
|
||||
},
|
||||
relations: ['calendarChannel', 'calendarChannel.connectedAccount'],
|
||||
relations: ['calendarChannel'],
|
||||
});
|
||||
|
||||
const connectedAccountRepository =
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import groupBy from 'lodash.groupby';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class CanAccessCalendarEventsService {
|
||||
constructor(
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
||||
private readonly workspaceMemberService: WorkspaceMemberRepository,
|
||||
) {}
|
||||
|
||||
public async canAccessCalendarEvents(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
calendarChannelCalendarEventAssociations: CalendarChannelEventAssociationWorkspaceEntity[],
|
||||
) {
|
||||
const calendarChannels = calendarChannelCalendarEventAssociations.map(
|
||||
(association) => association.calendarChannel,
|
||||
);
|
||||
|
||||
const calendarChannelsGroupByVisibility = groupBy(
|
||||
calendarChannels,
|
||||
(channel) => channel.visibility,
|
||||
);
|
||||
|
||||
if (
|
||||
calendarChannelsGroupByVisibility[
|
||||
CalendarChannelVisibility.SHARE_EVERYTHING
|
||||
] ||
|
||||
calendarChannelsGroupByVisibility[CalendarChannelVisibility.METADATA]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWorkspaceMember =
|
||||
await this.workspaceMemberService.getByIdOrFail(userId, workspaceId);
|
||||
|
||||
const connectedAccountRepository =
|
||||
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
const connectedAccounts = await connectedAccountRepository.find({
|
||||
select: ['id'],
|
||||
where: {
|
||||
calendarChannels: {
|
||||
id: In(calendarChannels.map((channel) => channel.id)),
|
||||
},
|
||||
accountOwnerId: currentWorkspaceMember.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (connectedAccounts.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ForbiddenError('Calendar events not shared');
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,8 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { CalendarEventFindManyPostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.post-query.hook';
|
||||
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
|
||||
import { CalendarEventFindOnePostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.post-query.hook';
|
||||
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';
|
||||
import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service';
|
||||
import { CanAccessCalendarEventsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-events.service';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Module({
|
||||
@ -14,10 +11,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
||||
],
|
||||
providers: [
|
||||
CanAccessCalendarEventsService,
|
||||
ApplyCalendarEventsVisibilityRestrictionsService,
|
||||
CalendarEventFindOnePreQueryHook,
|
||||
CalendarEventFindManyPreQueryHook,
|
||||
CalendarEventFindOnePostQueryHook,
|
||||
CalendarEventFindManyPostQueryHook,
|
||||
],
|
||||
|
||||
@ -0,0 +1,269 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
import { ApplyMessagesVisibilityRestrictionsService } from './apply-messages-visibility-restrictions.service';
|
||||
|
||||
const createMockMessage = (
|
||||
id: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
): MessageWorkspaceEntity => ({
|
||||
id,
|
||||
subject,
|
||||
text,
|
||||
headerMessageId: '',
|
||||
receivedAt: new Date('2024-03-20T10:00:00Z'),
|
||||
messageThreadId: '',
|
||||
messageThread: null,
|
||||
messageChannelMessageAssociations: [],
|
||||
messageParticipants: [],
|
||||
deletedAt: null,
|
||||
createdAt: '2024-03-20T09:00:00Z',
|
||||
updatedAt: '2024-03-20T09:00:00Z',
|
||||
});
|
||||
|
||||
describe('ApplyMessagesVisibilityRestrictionsService', () => {
|
||||
let service: ApplyMessagesVisibilityRestrictionsService;
|
||||
|
||||
const mockMessageChannelMessageAssociationRepository = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockConnectedAccountRepository = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockTwentyORMManager = {
|
||||
getRepository: jest.fn().mockImplementation((name) => {
|
||||
if (name === 'messageChannelMessageAssociation') {
|
||||
return mockMessageChannelMessageAssociationRepository;
|
||||
}
|
||||
if (name === 'connectedAccount') {
|
||||
return mockConnectedAccountRepository;
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ApplyMessagesVisibilityRestrictionsService,
|
||||
{
|
||||
provide: TwentyORMManager,
|
||||
useValue: mockTwentyORMManager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ApplyMessagesVisibilityRestrictionsService>(
|
||||
ApplyMessagesVisibilityRestrictionsService,
|
||||
);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return message without obfuscated subject and text if the visibility is SHARE_EVERYTHING', async () => {
|
||||
const messages = [
|
||||
createMockMessage('messageId', 'Test Subject', 'Test Message'),
|
||||
];
|
||||
|
||||
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
|
||||
{
|
||||
messageId: 'messageId',
|
||||
messageChannel: {
|
||||
id: 'messageChannelId',
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await service.applyMessagesVisibilityRestrictions(
|
||||
'workspace-member-id',
|
||||
messages,
|
||||
);
|
||||
|
||||
expect(result).toEqual(messages);
|
||||
expect(
|
||||
result.every(
|
||||
(item) =>
|
||||
item.subject === 'Test Subject' && item.text === 'Test Message',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(mockConnectedAccountRepository.find).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return message without obfuscated subject and with obfuscated text if the visibility is SUBJECT', async () => {
|
||||
const messages = [
|
||||
createMockMessage('messageId', 'Test Subject', 'Test Message'),
|
||||
];
|
||||
|
||||
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
|
||||
{
|
||||
messageId: 'messageId',
|
||||
messageChannel: {
|
||||
id: 'messageChannelId',
|
||||
visibility: MessageChannelVisibility.SUBJECT,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.applyMessagesVisibilityRestrictions(
|
||||
'workspace-member-id',
|
||||
messages,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...messages[0],
|
||||
text: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return message with obfuscated subject and text if the visibility is METADATA', async () => {
|
||||
const messages = [
|
||||
createMockMessage('messageId', 'Test Subject', 'Test Message'),
|
||||
];
|
||||
|
||||
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
|
||||
{
|
||||
messageId: 'messageId',
|
||||
messageChannel: {
|
||||
id: 'messageChannelId',
|
||||
visibility: MessageChannelVisibility.METADATA,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.applyMessagesVisibilityRestrictions(
|
||||
'workspace-member-id',
|
||||
messages,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...messages[0],
|
||||
subject: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||
text: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return message without obfuscated subject and text if the visibility is METADATA and the workspace member is the channel owner', async () => {
|
||||
const messages = [
|
||||
createMockMessage('messageId', 'Test Subject', 'Test Message'),
|
||||
];
|
||||
|
||||
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
|
||||
{
|
||||
messageId: 'messageId',
|
||||
messageChannel: {
|
||||
id: 'messageChannelId',
|
||||
visibility: MessageChannelVisibility.METADATA,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
|
||||
|
||||
const result = await service.applyMessagesVisibilityRestrictions(
|
||||
'workspace-member-id',
|
||||
messages,
|
||||
);
|
||||
|
||||
expect(result).toEqual(messages);
|
||||
expect(
|
||||
result.every(
|
||||
(item) =>
|
||||
item.subject === 'Test Subject' && item.text === 'Test Message',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should not return message if visibility is not SHARE_EVERYTHING, SUBJECT or METADATA and the workspace member is not the channel owner', async () => {
|
||||
const messages = [
|
||||
createMockMessage('messageId', 'Test Subject', 'Test Message'),
|
||||
];
|
||||
|
||||
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
|
||||
{
|
||||
messageId: 'messageId',
|
||||
messageChannel: {
|
||||
id: 'messageChannelId',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.applyMessagesVisibilityRestrictions(
|
||||
'workspace-member-id',
|
||||
messages,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return all messages with the right visibility', async () => {
|
||||
const messages = [
|
||||
createMockMessage('1', 'Subject 1', 'Message 1'),
|
||||
createMockMessage('2', 'Subject 2', 'Message 2'),
|
||||
createMockMessage('3', 'Subject 3', 'Message 3'),
|
||||
];
|
||||
|
||||
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
|
||||
{
|
||||
messageId: '1',
|
||||
messageChannel: {
|
||||
id: '1',
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: '2',
|
||||
messageChannel: {
|
||||
id: '2',
|
||||
visibility: MessageChannelVisibility.SUBJECT,
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: '3',
|
||||
messageChannel: {
|
||||
id: '3',
|
||||
visibility: MessageChannelVisibility.METADATA,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockConnectedAccountRepository.find
|
||||
.mockResolvedValueOnce([]) // request for message 3
|
||||
.mockResolvedValueOnce([]); // request for message 2
|
||||
|
||||
const result = await service.applyMessagesVisibilityRestrictions(
|
||||
'workspace-member-id',
|
||||
messages,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
messages[0],
|
||||
{
|
||||
...messages[1],
|
||||
text: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||
},
|
||||
{
|
||||
...messages[2],
|
||||
subject: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||
text: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,98 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import groupBy from 'lodash.groupby';
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ApplyMessagesVisibilityRestrictionsService {
|
||||
constructor(private readonly twentyORMManager: TwentyORMManager) {}
|
||||
|
||||
public async applyMessagesVisibilityRestrictions(
|
||||
workspaceMemberId: string,
|
||||
messages: MessageWorkspaceEntity[],
|
||||
) {
|
||||
const messageChannelMessageAssociationRepository =
|
||||
await this.twentyORMManager.getRepository<MessageChannelMessageAssociationWorkspaceEntity>(
|
||||
'messageChannelMessageAssociation',
|
||||
);
|
||||
|
||||
const messageChannelMessagesAssociations =
|
||||
await messageChannelMessageAssociationRepository.find({
|
||||
where: {
|
||||
messageId: In(messages.map((message) => message.id)),
|
||||
},
|
||||
relations: ['messageChannel'],
|
||||
});
|
||||
|
||||
const connectedAccountRepository =
|
||||
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const messageChannelMessageAssociations =
|
||||
messageChannelMessagesAssociations.filter(
|
||||
(association) => association.messageId === messages[i].id,
|
||||
);
|
||||
|
||||
const messageChannels = messageChannelMessageAssociations
|
||||
.map((association) => association.messageChannel)
|
||||
.filter(
|
||||
(channel): channel is NonNullable<typeof channel> => channel !== null,
|
||||
);
|
||||
|
||||
if (messageChannels.length === 0) {
|
||||
throw new NotFoundError('Associated message channels not found');
|
||||
}
|
||||
|
||||
const messageChannelsGroupByVisibility = groupBy(
|
||||
messageChannels,
|
||||
(channel) => channel.visibility,
|
||||
);
|
||||
|
||||
if (
|
||||
messageChannelsGroupByVisibility[
|
||||
MessageChannelVisibility.SHARE_EVERYTHING
|
||||
]
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const connectedAccounts = await connectedAccountRepository.find({
|
||||
select: ['id'],
|
||||
where: {
|
||||
messageChannels: {
|
||||
id: In(messageChannels.map((channel) => channel.id)),
|
||||
},
|
||||
accountOwnerId: workspaceMemberId,
|
||||
},
|
||||
});
|
||||
|
||||
if (connectedAccounts.length > 0) {
|
||||
continue;
|
||||
}
|
||||
if (messageChannelsGroupByVisibility[MessageChannelVisibility.SUBJECT]) {
|
||||
messages[i].text = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (messageChannelsGroupByVisibility[MessageChannelVisibility.METADATA]) {
|
||||
messages[i].subject = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
|
||||
messages[i].text = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
|
||||
continue;
|
||||
}
|
||||
|
||||
messages.splice(i, 1);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
import { ForbiddenException } from '@nestjs/common';
|
||||
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
export class CanAccessMessageThreadService {
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
||||
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
) {}
|
||||
|
||||
public async canAccessMessageThread(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
messageChannelMessageAssociations: MessageChannelMessageAssociationWorkspaceEntity[],
|
||||
) {
|
||||
const messageChannelIds = messageChannelMessageAssociations.map(
|
||||
(association) => association.messageChannelId,
|
||||
);
|
||||
|
||||
const currentWorkspaceMember =
|
||||
await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId);
|
||||
|
||||
const connectedAccountRepository =
|
||||
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
const connectedAccounts = await connectedAccountRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: [
|
||||
{
|
||||
messageChannels: {
|
||||
id: In(messageChannelIds),
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
},
|
||||
},
|
||||
{
|
||||
messageChannels: {
|
||||
id: In(messageChannelIds),
|
||||
},
|
||||
accountOwnerId: currentWorkspaceMember.id,
|
||||
},
|
||||
],
|
||||
take: 1,
|
||||
});
|
||||
|
||||
if (connectedAccounts.length === 0) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import { WorkspaceQueryPostHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
@WorkspaceQueryHook({
|
||||
key: `message.findMany`,
|
||||
type: WorkspaceQueryHookType.PostHook,
|
||||
})
|
||||
export class MessageFindManyPostQueryHook
|
||||
implements WorkspaceQueryPostHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly applyMessagesVisibilityRestrictionsService: ApplyMessagesVisibilityRestrictionsService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
_objectName: string,
|
||||
payload: MessageWorkspaceEntity[],
|
||||
): Promise<void> {
|
||||
if (!authContext.workspaceMemberId) {
|
||||
throw new UserInputError('Workspace member id is required');
|
||||
}
|
||||
|
||||
await this.applyMessagesVisibilityRestrictionsService.applyMessagesVisibilityRestrictions(
|
||||
authContext.workspaceMemberId,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
|
||||
@WorkspaceQueryHook(`message.findMany`)
|
||||
export class MessageFindManyPreQueryHook implements WorkspaceQueryHookInstance {
|
||||
constructor(
|
||||
private readonly canAccessMessageThreadService: CanAccessMessageThreadService,
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
objectName: string,
|
||||
payload: FindManyResolverArgs,
|
||||
): Promise<FindManyResolverArgs> {
|
||||
if (!payload?.filter?.messageThreadId?.eq) {
|
||||
throw new BadRequestException('messageThreadId filter is required');
|
||||
}
|
||||
|
||||
if (!authContext.user?.id) {
|
||||
throw new BadRequestException('User id is required');
|
||||
}
|
||||
|
||||
const messageChannelMessageAssociationRepository =
|
||||
await this.twentyORMManager.getRepository<MessageChannelMessageAssociationWorkspaceEntity>(
|
||||
'messageChannelMessageAssociation',
|
||||
);
|
||||
|
||||
const messageChannelMessageAssociations =
|
||||
await messageChannelMessageAssociationRepository.find({
|
||||
where: {
|
||||
message: {
|
||||
messageThreadId: payload.filter.messageThreadId.eq,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (messageChannelMessageAssociations.length === 0) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await this.canAccessMessageThreadService.canAccessMessageThread(
|
||||
authContext.user.id,
|
||||
authContext.workspace.id,
|
||||
messageChannelMessageAssociations,
|
||||
);
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import { WorkspaceQueryPostHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
@WorkspaceQueryHook({
|
||||
key: `message.findOne`,
|
||||
type: WorkspaceQueryHookType.PostHook,
|
||||
})
|
||||
export class MessageFindOnePostQueryHook
|
||||
implements WorkspaceQueryPostHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly applyMessagesVisibilityRestrictionsService: ApplyMessagesVisibilityRestrictionsService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
_objectName: string,
|
||||
payload: MessageWorkspaceEntity[],
|
||||
): Promise<void> {
|
||||
if (!authContext.workspaceMemberId) {
|
||||
throw new UserInputError('Workspace member id is required');
|
||||
}
|
||||
|
||||
await this.applyMessagesVisibilityRestrictionsService.applyMessagesVisibilityRestrictions(
|
||||
authContext.workspaceMemberId,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
|
||||
@WorkspaceQueryHook(`message.findOne`)
|
||||
export class MessageFindOnePreQueryHook implements WorkspaceQueryHookInstance {
|
||||
constructor(
|
||||
private readonly canAccessMessageThreadService: CanAccessMessageThreadService,
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
objectName: string,
|
||||
payload: FindOneResolverArgs,
|
||||
): Promise<FindOneResolverArgs> {
|
||||
if (!authContext.user?.id) {
|
||||
throw new NotFoundException('User id is required');
|
||||
}
|
||||
|
||||
const messageChannelMessageAssociationRepository =
|
||||
await this.twentyORMManager.getRepository<MessageChannelMessageAssociationWorkspaceEntity>(
|
||||
'messageChannelMessageAssociation',
|
||||
);
|
||||
|
||||
const messageChannelMessageAssociations =
|
||||
await messageChannelMessageAssociationRepository.find({
|
||||
where: {
|
||||
messageId: payload?.filter?.id?.eq,
|
||||
},
|
||||
});
|
||||
|
||||
if (messageChannelMessageAssociations.length === 0) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await this.canAccessMessageThreadService.canAccessMessageThread(
|
||||
authContext.user.id,
|
||||
authContext.workspace.id,
|
||||
messageChannelMessageAssociations,
|
||||
);
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
|
||||
import { MessageFindManyPreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook';
|
||||
import { MessageFindOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook';
|
||||
import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
|
||||
import { MessageFindManyPostQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.post-query.hook';
|
||||
import { MessageFindOnePostQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.post-query.hook';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Module({
|
||||
@ -11,9 +11,9 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
||||
],
|
||||
providers: [
|
||||
CanAccessMessageThreadService,
|
||||
MessageFindOnePreQueryHook,
|
||||
MessageFindManyPreQueryHook,
|
||||
ApplyMessagesVisibilityRestrictionsService,
|
||||
MessageFindOnePostQueryHook,
|
||||
MessageFindManyPostQueryHook,
|
||||
],
|
||||
})
|
||||
export class MessagingQueryHookModule {}
|
||||
|
||||
Reference in New Issue
Block a user