From f9c076df319609e530dc8ac0431dd028564d5332 Mon Sep 17 00:00:00 2001 From: brendanlaschke Date: Thu, 7 Nov 2024 18:13:22 +0100 Subject: [PATCH] o365 calendar sync (#8044) Implemented: * Account Connect * Calendar sync via delta ids then requesting single events I think I would split the messaging part into a second pr - that's a step more complex then the calendar :) --------- Co-authored-by: bosiraphael --- package.json | 2 + ...tionBannerReconnectAccountEmailAliases.tsx | 6 +- ...econnectAccountInsufficientPermissions.tsx | 6 +- ...tingsAccountsConnectedAccountsListCard.tsx | 9 +- .../SettingsAccountsListEmptyStateCard.tsx | 33 ++- .../SettingsAccountsRowDropdownMenu.tsx | 7 +- ...ogleApisOAuth.ts => useTriggerApiOAuth.ts} | 44 ++-- .../WorkflowEditActionFormSendEmail.tsx | 9 +- .../modules/workspace/types/FeatureFlagKey.ts | 1 + .../src/pages/onboarding/SyncEmails.tsx | 6 +- packages/twenty-server/.env.example | 1 + .../typeorm-seeds/core/feature-flags.ts | 5 + .../engine/core-modules/auth/auth.module.ts | 4 + .../microsoft-apis-auth.controller.ts | 105 +++++++++ ...pis-oauth-exchange-code-for-token.guard.ts | 31 +++ ...mircosoft-apis-oauth-request-code.guard.ts | 62 +++++ .../auth/services/google-apis.service.ts | 10 +- .../auth/services/microsoft-apis.service.ts | 212 ++++++++++++++++++ ...crosoft-apis-oauth-common.auth.strategy.ts | 31 +++ ...h-exchange-code-for-token.auth.strategy.ts | 48 ++++ ...t-apis-oauth-request-code.auth.strategy.ts | 28 +++ .../auth/types/microsoft-api-request.type.ts | 23 ++ .../utils/get-microsoft-apis-oauth-scopes.ts | 12 + .../environment/environment-variables.ts | 4 + .../enums/feature-flag-key.enum.ts | 1 + .../engine/core-modules/user/user.resolver.ts | 1 - .../calendar-event-import-manager.module.ts | 12 +- .../calendar-event-import-batch-size.ts | 1 + .../calendar-event-list-fetch.cron.command.ts | 6 +- .../commands/calendar-import.cron.command.ts | 32 +++ .../calendar-event-list-fetch.cron.job.ts | 8 +- .../jobs/calendar-events-import.cron.job.ts | 87 +++++++ .../google-calendar-get-events.service.ts | 1 + .../microsoft-calendar-driver.module.ts | 20 ++ .../microsoft-calendar-get-events.service.ts | 62 +++++ ...icrosoft-calendar-import-events.service.ts | 45 ++++ .../format-microsoft-calendar-event.util.ts | 60 +++++ .../parse-microsoft-calendar-error.util.ts | 65 ++++++ .../jobs/calendar-event-list-fetch.job.ts | 12 +- .../jobs/calendar-events-import.job.ts | 75 +++++++ .../calendar-events-import.service.ts | 89 ++++---- .../services/calendar-fetch-events.service.ts | 129 +++++++++++ .../services/calendar-get-events.service.ts | 17 +- .../utils/filter-events.util.ts | 2 +- ...microsoft-oauth2-client-manager.service.ts | 62 +++++ .../oauth2-client-manager.module.ts | 9 +- .../connected-account.workspace-entity.ts | 1 + .../messaging-get-message-list.service.ts | 12 + .../self-hosting/self-hosting-var.mdx | 1 + yarn.lock | 28 +++ 50 files changed, 1418 insertions(+), 119 deletions(-) rename packages/twenty-front/src/modules/settings/accounts/hooks/{useTriggerGoogleApisOAuth.ts => useTriggerApiOAuth.ts} (55%) create mode 100644 packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-exchange-code-for-token.guard.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/guards/mircosoft-apis-oauth-request-code.guard.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft-apis-oauth-common.auth.strategy.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft-apis-oauth-exchange-code-for-token.auth.strategy.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft-apis-oauth-request-code.auth.strategy.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/types/microsoft-api-request.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes.ts create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/constants/calendar-event-import-batch-size.ts create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-import.cron.command.ts create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job.ts create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/microsoft-calendar-driver.module.ts create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-get-events.service.ts create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service.ts create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util.ts create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts create mode 100644 packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service.ts diff --git a/package.json b/package.json index 41e8e4b35b23..c9c7f6f1ef34 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@linaria/core": "^6.2.0", "@linaria/react": "^6.2.1", "@mdx-js/react": "^3.0.0", + "@microsoft/microsoft-graph-client": "^3.0.7", "@nestjs/apollo": "^11.0.5", "@nestjs/axios": "^3.0.1", "@nestjs/cli": "^9.0.0", @@ -201,6 +202,7 @@ "@graphql-codegen/typescript": "^3.0.4", "@graphql-codegen/typescript-operations": "^3.0.4", "@graphql-codegen/typescript-react-apollo": "^3.3.7", + "@microsoft/microsoft-graph-types": "^2.40.0", "@nestjs/cli": "^9.0.0", "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^9.0.0", diff --git a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx index c3f381c1bcd5..67fc042a92e2 100644 --- a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx @@ -1,7 +1,7 @@ import { InformationBanner } from '@/information-banner/components/InformationBanner'; import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect'; import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys'; -import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; +import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; import { IconRefresh } from 'twenty-ui'; export const InformationBannerReconnectAccountEmailAliases = () => { @@ -9,7 +9,7 @@ export const InformationBannerReconnectAccountEmailAliases = () => { InformationBannerKeys.ACCOUNTS_TO_RECONNECT_EMAIL_ALIASES, ); - const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth(); + const { triggerApisOAuth } = useTriggerApisOAuth(); if (!accountToReconnect) { return null; @@ -20,7 +20,7 @@ export const InformationBannerReconnectAccountEmailAliases = () => { message={`Please reconnect your mailbox ${accountToReconnect?.handle} to update your email aliases:`} buttonTitle="Reconnect" buttonIcon={IconRefresh} - buttonOnClick={() => triggerGoogleApisOAuth()} + buttonOnClick={() => triggerApisOAuth(accountToReconnect.provider)} /> ); }; diff --git a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx index 7f74a129b652..306452b56292 100644 --- a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx @@ -1,7 +1,7 @@ import { InformationBanner } from '@/information-banner/components/InformationBanner'; import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect'; import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys'; -import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; +import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; import { IconRefresh } from 'twenty-ui'; export const InformationBannerReconnectAccountInsufficientPermissions = () => { @@ -9,7 +9,7 @@ export const InformationBannerReconnectAccountInsufficientPermissions = () => { InformationBannerKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS, ); - const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth(); + const { triggerApisOAuth } = useTriggerApisOAuth(); if (!accountToReconnect) { return null; @@ -21,7 +21,7 @@ export const InformationBannerReconnectAccountInsufficientPermissions = () => { reconnect for updates:`} buttonTitle="Reconnect" buttonIcon={IconRefresh} - buttonOnClick={() => triggerGoogleApisOAuth()} + buttonOnClick={() => triggerApisOAuth(accountToReconnect.provider)} /> ); }; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsListCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsListCard.tsx index 238371241d0a..6000afa1fe98 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsListCard.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsListCard.tsx @@ -1,5 +1,5 @@ import { useNavigate } from 'react-router-dom'; -import { IconGoogle } from 'twenty-ui'; +import { IconComponent, IconGoogle, IconMicrosoft } from 'twenty-ui'; import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/components/SettingsAccountsListEmptyStateCard'; @@ -9,6 +9,11 @@ import { SettingsPath } from '@/types/SettingsPath'; import { SettingsAccountsConnectedAccountsRowRightContainer } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer'; import { SettingsListCard } from '../../components/SettingsListCard'; +const ProviderIcons: { [k: string]: IconComponent } = { + google: IconGoogle, + microsoft: IconMicrosoft, +}; + export const SettingsAccountsConnectedAccountsListCard = ({ accounts, loading, @@ -27,7 +32,7 @@ export const SettingsAccountsConnectedAccountsListCard = ({ items={accounts} getItemLabel={(account) => account.handle} isLoading={loading} - RowIcon={IconGoogle} + RowIconFn={(row) => ProviderIcons[row.provider]} RowRightComponent={({ item: account }) => ( )} diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx index 8500264c4125..d532691fcc5b 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx @@ -1,7 +1,14 @@ +import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import styled from '@emotion/styled'; -import { Button, Card, CardContent, CardHeader, IconGoogle } from 'twenty-ui'; - -import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; +import { + Button, + Card, + CardContent, + CardHeader, + IconGoogle, + IconMicrosoft, +} from 'twenty-ui'; const StyledHeader = styled(CardHeader)` align-items: center; @@ -12,6 +19,7 @@ const StyledHeader = styled(CardHeader)` const StyledBody = styled(CardContent)` display: flex; justify-content: center; + gap: ${({ theme }) => theme.spacing(2)}; `; type SettingsAccountsListEmptyStateCardProps = { @@ -21,11 +29,10 @@ type SettingsAccountsListEmptyStateCardProps = { export const SettingsAccountsListEmptyStateCard = ({ label, }: SettingsAccountsListEmptyStateCardProps) => { - const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth(); - - const handleOnClick = async () => { - await triggerGoogleApisOAuth(); - }; + const { triggerApisOAuth } = useTriggerApisOAuth(); + const isMicrosoftSyncEnabled = useIsFeatureEnabled( + 'IS_MICROSOFT_SYNC_ENABLED', + ); return ( @@ -35,8 +42,16 @@ export const SettingsAccountsListEmptyStateCard = ({ Icon={IconGoogle} title="Connect with Google" variant="secondary" - onClick={handleOnClick} + onClick={() => triggerApisOAuth('google')} /> + {isMicrosoftSyncEnabled && ( +