diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index ef50a7fa13..bb598e60c9 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -860,6 +860,7 @@ export type Object = { imageIdentifierFieldMetadataId?: Maybe; isActive: Scalars['Boolean']; isCustom: Scalars['Boolean']; + isRemote: Scalars['Boolean']; isSystem: Scalars['Boolean']; labelIdentifierFieldMetadataId?: Maybe; labelPlural: Scalars['String']; @@ -905,9 +906,9 @@ export type RelationEdge = { node: Relation; }; -export type AttendeeFragmentFragment = { __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }; +export type TimelineCalendarEventAttendeeFragmentFragment = { __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }; -export type CalendarEventFragmentFragment = { __typename?: 'TimelineCalendarEvent', id: string, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: TimelineCalendarEventVisibility, attendees: Array<{ __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }; +export type TimelineCalendarEventFragmentFragment = { __typename?: 'TimelineCalendarEvent', id: string, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: TimelineCalendarEventVisibility, attendees: Array<{ __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }; export type TimelineCalendarEventsWithTotalFragmentFragment = { __typename?: 'TimelineCalendarEventsWithTotal', totalNumberOfCalendarEvents: number, timelineCalendarEvents: Array<{ __typename?: 'TimelineCalendarEvent', id: string, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: TimelineCalendarEventVisibility, attendees: Array<{ __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> }; @@ -1154,8 +1155,8 @@ export type GetWorkspaceFromInviteHashQueryVariables = Exact<{ export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } }; -export const AttendeeFragmentFragmentDoc = gql` - fragment AttendeeFragment on TimelineCalendarEventAttendee { +export const TimelineCalendarEventAttendeeFragmentFragmentDoc = gql` + fragment TimelineCalendarEventAttendeeFragment on TimelineCalendarEventAttendee { personId workspaceMemberId firstName @@ -1165,8 +1166,8 @@ export const AttendeeFragmentFragmentDoc = gql` handle } `; -export const CalendarEventFragmentFragmentDoc = gql` - fragment CalendarEventFragment on TimelineCalendarEvent { +export const TimelineCalendarEventFragmentFragmentDoc = gql` + fragment TimelineCalendarEventFragment on TimelineCalendarEvent { id title description @@ -1176,18 +1177,18 @@ export const CalendarEventFragmentFragmentDoc = gql` isFullDay visibility attendees { - ...AttendeeFragment + ...TimelineCalendarEventAttendeeFragment } } - ${AttendeeFragmentFragmentDoc}`; + ${TimelineCalendarEventAttendeeFragmentFragmentDoc}`; export const TimelineCalendarEventsWithTotalFragmentFragmentDoc = gql` fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal { totalNumberOfCalendarEvents timelineCalendarEvents { - ...CalendarEventFragment + ...TimelineCalendarEventFragment } } - ${CalendarEventFragmentFragmentDoc}`; + ${TimelineCalendarEventFragmentFragmentDoc}`; export const ParticipantFragmentFragmentDoc = gql` fragment ParticipantFragment on TimelineThreadParticipant { personId diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragment.ts b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragment.ts deleted file mode 100644 index 6a6e0f54a2..0000000000 --- a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragment.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { gql } from '@apollo/client'; - -import { attendeeFragment } from '@/activities/calendar/queries/fragments/attendeeFragment'; - -export const calendarEventFragment = gql` - fragment CalendarEventFragment on TimelineCalendarEvent { - id - title - description - location - startsAt - endsAt - isFullDay - visibility - attendees { - ...AttendeeFragment - } - } - ${attendeeFragment} -`; diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/attendeeFragment.ts b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventAttendeeFragment.ts similarity index 51% rename from packages/twenty-front/src/modules/activities/calendar/queries/fragments/attendeeFragment.ts rename to packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventAttendeeFragment.ts index fc6b600dd3..5cc1e46f38 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/attendeeFragment.ts +++ b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventAttendeeFragment.ts @@ -1,7 +1,7 @@ import { gql } from '@apollo/client'; -export const attendeeFragment = gql` - fragment AttendeeFragment on TimelineCalendarEventAttendee { +export const timelineCalendarEventAttendeeFragment = gql` + fragment TimelineCalendarEventAttendeeFragment on TimelineCalendarEventAttendee { personId workspaceMemberId firstName diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventFragment.ts b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventFragment.ts new file mode 100644 index 0000000000..634b7d5c26 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventFragment.ts @@ -0,0 +1,20 @@ +import { gql } from '@apollo/client'; + +import { timelineCalendarEventAttendeeFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventAttendeeFragment'; + +export const timelineCalendarEventFragment = gql` + fragment TimelineCalendarEventFragment on TimelineCalendarEvent { + id + title + description + location + startsAt + endsAt + isFullDay + visibility + attendees { + ...TimelineCalendarEventAttendeeFragment + } + } + ${timelineCalendarEventAttendeeFragment} +`; diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment.ts b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment.ts similarity index 57% rename from packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment.ts rename to packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment.ts index 5788bb09c5..2a76f0f7fa 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment.ts +++ b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment.ts @@ -1,13 +1,13 @@ import { gql } from '@apollo/client'; -import { calendarEventFragment } from '@/activities/calendar/queries/fragments/calendarEventFragment'; +import { timelineCalendarEventFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventFragment'; export const timelineCalendarEventWithTotalFragment = gql` fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal { totalNumberOfCalendarEvents timelineCalendarEvents { - ...CalendarEventFragment + ...TimelineCalendarEventFragment } } - ${calendarEventFragment} + ${timelineCalendarEventFragment} `; diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId.ts b/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId.ts index 1928fded25..f0c74e4d5b 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId.ts +++ b/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId.ts @@ -1,6 +1,6 @@ import { gql } from '@apollo/client'; -import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment'; +import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment'; export const getTimelineCalendarEventsFromCompanyId = gql` query GetTimelineCalendarEventsFromCompanyId( diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromPersonId.ts b/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromPersonId.ts index 764282a6be..057430d477 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromPersonId.ts +++ b/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromPersonId.ts @@ -1,6 +1,6 @@ import { gql } from '@apollo/client'; -import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment'; +import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment'; export const getTimelineCalendarEventsFromPersonId = gql` query GetTimelineCalendarEventsFromPersonId( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts index c0472264fe..72361faed6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts @@ -1,6 +1,8 @@ import { MessageFindManyPreQueryHook } from 'src/modules/messaging/query-hooks/message/message-find-many.pre-query.hook'; import { MessageFindOnePreQueryHook } from 'src/modules/messaging/query-hooks/message/message-find-one.pre-query-hook'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type'; +import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook'; +import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook'; // TODO: move to a decorator export const workspacePreQueryHooks: WorkspaceQueryHook = { @@ -8,4 +10,8 @@ export const workspacePreQueryHooks: WorkspaceQueryHook = { findOne: [MessageFindOnePreQueryHook.name], findMany: [MessageFindManyPreQueryHook.name], }, + calendarEvent: { + findOne: [CalendarEventFindOnePreQueryHook.name], + findMany: [CalendarEventFindManyPreQueryHook.name], + }, }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts index 3887e39e56..90f841b62a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { MessagingQueryHookModule } from 'src/modules/messaging/query-hooks/messaging-query-hook.module'; import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service'; +import { CalendarQueryHookModule } from 'src/modules/calendar/query-hooks/calendar-query-hook.module'; @Module({ - imports: [MessagingQueryHookModule], + imports: [MessagingQueryHookModule, CalendarQueryHookModule], providers: [WorkspacePreQueryHookService], exports: [WorkspacePreQueryHookService], }) diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook.ts new file mode 100644 index 0000000000..115d4cbe3f --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook.ts @@ -0,0 +1,52 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; + +import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; +import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; + +import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata'; +import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository'; +import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service'; + +@Injectable() +export class CalendarEventFindManyPreQueryHook + implements WorkspacePreQueryHook +{ + constructor( + @InjectObjectMetadataRepository( + CalendarChannelEventAssociationObjectMetadata, + ) + private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository, + private readonly canAccessCalendarEventService: CanAccessCalendarEventService, + ) {} + + async execute( + userId: string, + workspaceId: string, + payload: FindManyResolverArgs, + ): Promise { + if (!payload?.filter?.id?.eq) { + throw new BadRequestException('id filter is required'); + } + + const calendarChannelCalendarEventAssociations = + await this.calendarChannelEventAssociationRepository.getByCalendarEventIds( + [payload?.filter?.id?.eq], + workspaceId, + ); + + if (calendarChannelCalendarEventAssociations.length === 0) { + throw new NotFoundException(); + } + + await this.canAccessCalendarEventService.canAccessCalendarEvent( + userId, + workspaceId, + calendarChannelCalendarEventAssociations, + ); + } +} diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts new file mode 100644 index 0000000000..cac0b8f887 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts @@ -0,0 +1,50 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; + +import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; +import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; + +import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service'; +import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository'; +import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata'; + +@Injectable() +export class CalendarEventFindOnePreQueryHook implements WorkspacePreQueryHook { + constructor( + @InjectObjectMetadataRepository( + CalendarChannelEventAssociationObjectMetadata, + ) + private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository, + private readonly canAccessCalendarEventService: CanAccessCalendarEventService, + ) {} + + async execute( + userId: string, + workspaceId: string, + payload: FindOneResolverArgs, + ): Promise { + if (!payload?.filter?.id?.eq) { + throw new BadRequestException('id filter is required'); + } + + const calendarChannelCalendarEventAssociations = + await this.calendarChannelEventAssociationRepository.getByCalendarEventIds( + [payload?.filter?.id?.eq], + workspaceId, + ); + + if (calendarChannelCalendarEventAssociations.length === 0) { + throw new NotFoundException(); + } + + await this.canAccessCalendarEventService.canAccessCalendarEvent( + userId, + workspaceId, + calendarChannelCalendarEventAssociations, + ); + } +} diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service.ts new file mode 100644 index 0000000000..8008db95a3 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service.ts @@ -0,0 +1,76 @@ +import { ForbiddenException, Injectable } from '@nestjs/common'; + +import groupBy from 'lodash.groupby'; + +import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record'; +import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository'; +import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata'; +import { + CalendarChannelObjectMetadata, + CalendarChannelVisibility, +} from 'src/modules/calendar/standard-objects/calendar-channel.object-metadata'; +import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; +import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata'; +import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; +import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; + +@Injectable() +export class CanAccessCalendarEventService { + constructor( + @InjectObjectMetadataRepository(CalendarChannelObjectMetadata) + private readonly calendarChannelRepository: CalendarChannelRepository, + @InjectObjectMetadataRepository(ConnectedAccountObjectMetadata) + private readonly connectedAccountRepository: ConnectedAccountRepository, + @InjectObjectMetadataRepository(WorkspaceMemberObjectMetadata) + private readonly workspaceMemberService: WorkspaceMemberRepository, + ) {} + + public async canAccessCalendarEvent( + userId: string, + workspaceId: string, + calendarChannelCalendarEventAssociations: ObjectRecord[], + ) { + const calendarChannels = await this.calendarChannelRepository.getByIds( + calendarChannelCalendarEventAssociations.map( + (association) => association.calendarChannelId, + ), + workspaceId, + ); + + const calendarChannelsGroupByVisibility = groupBy( + calendarChannels, + (channel) => channel.visibility, + ); + + if ( + calendarChannelsGroupByVisibility[ + CalendarChannelVisibility.SHARE_EVERYTHING + ] + ) { + return; + } + + const currentWorkspaceMember = + await this.workspaceMemberService.getByIdOrFail(userId, workspaceId); + + const calendarChannelsConnectedAccounts = + await this.connectedAccountRepository.getByIds( + calendarChannels.map((channel) => channel.connectedAccountId), + workspaceId, + ); + + const calendarChannelsWorkspaceMemberIds = + calendarChannelsConnectedAccounts.map( + (connectedAccount) => connectedAccount.accountOwnerId, + ); + + if ( + calendarChannelsWorkspaceMemberIds.includes(currentWorkspaceMember.id) + ) { + return; + } + + throw new ForbiddenException(); + } +} diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-query-hook.module.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-query-hook.module.ts new file mode 100644 index 0000000000..1e116c00e3 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-query-hook.module.ts @@ -0,0 +1,33 @@ +import { Module } from '@nestjs/common'; + +import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; +import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata'; +import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata'; +import { CalendarChannelObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel.object-metadata'; +import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook'; +import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook'; +import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service'; + +@Module({ + imports: [ + ObjectMetadataRepositoryModule.forFeature([ + CalendarChannelEventAssociationObjectMetadata, + CalendarChannelObjectMetadata, + ConnectedAccountObjectMetadata, + WorkspaceMemberObjectMetadata, + ]), + ], + providers: [ + CanAccessCalendarEventService, + { + provide: CalendarEventFindOnePreQueryHook.name, + useClass: CalendarEventFindOnePreQueryHook, + }, + { + provide: CalendarEventFindManyPreQueryHook.name, + useClass: CalendarEventFindManyPreQueryHook, + }, + ], +}) +export class CalendarQueryHookModule {}