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 styled from '@emotion/styled';
|
||||||
import { isUndefined } from '@sniptt/guards';
|
|
||||||
|
|
||||||
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
|
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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
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 { isDefined } from 'twenty-shared/utils';
|
||||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||||
|
|
||||||
@ -85,7 +86,7 @@ export const EventCardMessage = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (shouldHideMessageContent) {
|
if (shouldHideMessageContent) {
|
||||||
return <EventCardMessageNotShared sharedByFullName={authorFullName} />;
|
return <EventCardMessageForbidden notSharedByFullName={authorFullName} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldHandleNotFound = error.graphQLErrors.some(
|
const shouldHandleNotFound = error.graphQLErrors.some(
|
||||||
@ -99,7 +100,7 @@ export const EventCardMessage = ({
|
|||||||
return <div>Error loading message</div>;
|
return <div>Error loading message</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading || isUndefined(message)) {
|
if (loading || !isDefined(message)) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,12 +113,21 @@ export const EventCardMessage = ({
|
|||||||
<StyledEventCardMessageContainer>
|
<StyledEventCardMessageContainer>
|
||||||
<StyledEmailContent>
|
<StyledEmailContent>
|
||||||
<StyledEmailTop>
|
<StyledEmailTop>
|
||||||
<StyledEmailTitle>{message.subject}</StyledEmailTitle>
|
<StyledEmailTitle>
|
||||||
|
{message.subject !==
|
||||||
|
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
|
||||||
|
? message.subject
|
||||||
|
: 'Subject not shared'}
|
||||||
|
</StyledEmailTitle>
|
||||||
<StyledEmailParticipants>
|
<StyledEmailParticipants>
|
||||||
<OverflowingTextWithTooltip text={messageParticipantHandles} />
|
<OverflowingTextWithTooltip text={messageParticipantHandles} />
|
||||||
</StyledEmailParticipants>
|
</StyledEmailParticipants>
|
||||||
</StyledEmailTop>
|
</StyledEmailTop>
|
||||||
<StyledEmailBody>{message.text}</StyledEmailBody>
|
{message.text !== FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED ? (
|
||||||
|
<StyledEmailBody>{message.text}</StyledEmailBody>
|
||||||
|
) : (
|
||||||
|
<EventCardMessageBodyNotShared notSharedByFullName={authorFullName} />
|
||||||
|
)}
|
||||||
</StyledEmailContent>
|
</StyledEmailContent>
|
||||||
</StyledEventCardMessageContainer>
|
</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 visibilityValues = Object.values(MessageChannelVisibility);
|
||||||
|
|
||||||
const threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants:
|
const threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotOwner:
|
||||||
| {
|
| {
|
||||||
[key: string]: MessageChannelVisibility;
|
[key: string]: MessageChannelVisibility;
|
||||||
}
|
}
|
||||||
@ -247,10 +247,10 @@ export class TimelineMessagingService {
|
|||||||
const threadVisibilityByThreadId: {
|
const threadVisibilityByThreadId: {
|
||||||
[key: string]: MessageChannelVisibility;
|
[key: string]: MessageChannelVisibility;
|
||||||
} = messageThreadIds.reduce((threadVisibilityAcc, messageThreadId) => {
|
} = 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] =
|
threadVisibilityAcc[messageThreadId] =
|
||||||
threadIdsWithoutWorkspaceMember.includes(messageThreadId)
|
threadIdsWithoutWorkspaceMember.includes(messageThreadId)
|
||||||
? (threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants?.[
|
? (threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotOwner?.[
|
||||||
messageThreadId
|
messageThreadId
|
||||||
] ?? MessageChannelVisibility.METADATA)
|
] ?? MessageChannelVisibility.METADATA)
|
||||||
: MessageChannelVisibility.SHARE_EVERYTHING;
|
: 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 { 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 { 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';
|
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||||
@ -19,10 +21,23 @@ export const formatThreads = (
|
|||||||
[key: string]: MessageChannelVisibility;
|
[key: string]: MessageChannelVisibility;
|
||||||
},
|
},
|
||||||
): TimelineThread[] => {
|
): TimelineThread[] => {
|
||||||
return threads.map((thread) => ({
|
return threads.map((thread) => {
|
||||||
...thread,
|
const visibility = threadVisibilityByThreadId[thread.id];
|
||||||
...extractParticipantSummary(threadParticipantsByThreadId[thread.id]),
|
|
||||||
visibility: threadVisibilityByThreadId[thread.id],
|
return {
|
||||||
read: true,
|
...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: {
|
where: {
|
||||||
calendarEventId: In(calendarEvents.map((event) => event.id)),
|
calendarEventId: In(calendarEvents.map((event) => event.id)),
|
||||||
},
|
},
|
||||||
relations: ['calendarChannel', 'calendarChannel.connectedAccount'],
|
relations: ['calendarChannel'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const connectedAccountRepository =
|
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 { 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 { 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 { 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 { 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';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -14,10 +11,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
|||||||
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CanAccessCalendarEventsService,
|
|
||||||
ApplyCalendarEventsVisibilityRestrictionsService,
|
ApplyCalendarEventsVisibilityRestrictionsService,
|
||||||
CalendarEventFindOnePreQueryHook,
|
|
||||||
CalendarEventFindManyPreQueryHook,
|
|
||||||
CalendarEventFindOnePostQueryHook,
|
CalendarEventFindOnePostQueryHook,
|
||||||
CalendarEventFindManyPostQueryHook,
|
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 { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
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 { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
|
||||||
import { MessageFindManyPreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook';
|
import { MessageFindManyPostQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.post-query.hook';
|
||||||
import { MessageFindOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.pre-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';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -11,9 +11,9 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
|||||||
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CanAccessMessageThreadService,
|
ApplyMessagesVisibilityRestrictionsService,
|
||||||
MessageFindOnePreQueryHook,
|
MessageFindOnePostQueryHook,
|
||||||
MessageFindManyPreQueryHook,
|
MessageFindManyPostQueryHook,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class MessagingQueryHookModule {}
|
export class MessagingQueryHookModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user