Skip to content

Commit f9c076d

Browse files
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 <[email protected]>
1 parent 83f3963 commit f9c076d

File tree

50 files changed

+1418
-119
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1418
-119
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@linaria/core": "^6.2.0",
2424
"@linaria/react": "^6.2.1",
2525
"@mdx-js/react": "^3.0.0",
26+
"@microsoft/microsoft-graph-client": "^3.0.7",
2627
"@nestjs/apollo": "^11.0.5",
2728
"@nestjs/axios": "^3.0.1",
2829
"@nestjs/cli": "^9.0.0",
@@ -201,6 +202,7 @@
201202
"@graphql-codegen/typescript": "^3.0.4",
202203
"@graphql-codegen/typescript-operations": "^3.0.4",
203204
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
205+
"@microsoft/microsoft-graph-types": "^2.40.0",
204206
"@nestjs/cli": "^9.0.0",
205207
"@nestjs/schematics": "^9.0.0",
206208
"@nestjs/testing": "^9.0.0",
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { InformationBanner } from '@/information-banner/components/InformationBanner';
22
import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect';
33
import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys';
4-
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
4+
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
55
import { IconRefresh } from 'twenty-ui';
66

77
export const InformationBannerReconnectAccountEmailAliases = () => {
88
const { accountToReconnect } = useAccountToReconnect(
99
InformationBannerKeys.ACCOUNTS_TO_RECONNECT_EMAIL_ALIASES,
1010
);
1111

12-
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
12+
const { triggerApisOAuth } = useTriggerApisOAuth();
1313

1414
if (!accountToReconnect) {
1515
return null;
@@ -20,7 +20,7 @@ export const InformationBannerReconnectAccountEmailAliases = () => {
2020
message={`Please reconnect your mailbox ${accountToReconnect?.handle} to update your email aliases:`}
2121
buttonTitle="Reconnect"
2222
buttonIcon={IconRefresh}
23-
buttonOnClick={() => triggerGoogleApisOAuth()}
23+
buttonOnClick={() => triggerApisOAuth(accountToReconnect.provider)}
2424
/>
2525
);
2626
};

packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { InformationBanner } from '@/information-banner/components/InformationBanner';
22
import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect';
33
import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys';
4-
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
4+
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
55
import { IconRefresh } from 'twenty-ui';
66

77
export const InformationBannerReconnectAccountInsufficientPermissions = () => {
88
const { accountToReconnect } = useAccountToReconnect(
99
InformationBannerKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS,
1010
);
1111

12-
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
12+
const { triggerApisOAuth } = useTriggerApisOAuth();
1313

1414
if (!accountToReconnect) {
1515
return null;
@@ -21,7 +21,7 @@ export const InformationBannerReconnectAccountInsufficientPermissions = () => {
2121
reconnect for updates:`}
2222
buttonTitle="Reconnect"
2323
buttonIcon={IconRefresh}
24-
buttonOnClick={() => triggerGoogleApisOAuth()}
24+
buttonOnClick={() => triggerApisOAuth(accountToReconnect.provider)}
2525
/>
2626
);
2727
};

packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsListCard.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useNavigate } from 'react-router-dom';
2-
import { IconGoogle } from 'twenty-ui';
2+
import { IconComponent, IconGoogle, IconMicrosoft } from 'twenty-ui';
33

44
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
55
import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/components/SettingsAccountsListEmptyStateCard';
@@ -9,6 +9,11 @@ import { SettingsPath } from '@/types/SettingsPath';
99
import { SettingsAccountsConnectedAccountsRowRightContainer } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer';
1010
import { SettingsListCard } from '../../components/SettingsListCard';
1111

12+
const ProviderIcons: { [k: string]: IconComponent } = {
13+
google: IconGoogle,
14+
microsoft: IconMicrosoft,
15+
};
16+
1217
export const SettingsAccountsConnectedAccountsListCard = ({
1318
accounts,
1419
loading,
@@ -27,7 +32,7 @@ export const SettingsAccountsConnectedAccountsListCard = ({
2732
items={accounts}
2833
getItemLabel={(account) => account.handle}
2934
isLoading={loading}
30-
RowIcon={IconGoogle}
35+
RowIconFn={(row) => ProviderIcons[row.provider]}
3136
RowRightComponent={({ item: account }) => (
3237
<SettingsAccountsConnectedAccountsRowRightContainer account={account} />
3338
)}

packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx

+24-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
2+
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
13
import styled from '@emotion/styled';
2-
import { Button, Card, CardContent, CardHeader, IconGoogle } from 'twenty-ui';
3-
4-
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
4+
import {
5+
Button,
6+
Card,
7+
CardContent,
8+
CardHeader,
9+
IconGoogle,
10+
IconMicrosoft,
11+
} from 'twenty-ui';
512

613
const StyledHeader = styled(CardHeader)`
714
align-items: center;
@@ -12,6 +19,7 @@ const StyledHeader = styled(CardHeader)`
1219
const StyledBody = styled(CardContent)`
1320
display: flex;
1421
justify-content: center;
22+
gap: ${({ theme }) => theme.spacing(2)};
1523
`;
1624

1725
type SettingsAccountsListEmptyStateCardProps = {
@@ -21,11 +29,10 @@ type SettingsAccountsListEmptyStateCardProps = {
2129
export const SettingsAccountsListEmptyStateCard = ({
2230
label,
2331
}: SettingsAccountsListEmptyStateCardProps) => {
24-
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
25-
26-
const handleOnClick = async () => {
27-
await triggerGoogleApisOAuth();
28-
};
32+
const { triggerApisOAuth } = useTriggerApisOAuth();
33+
const isMicrosoftSyncEnabled = useIsFeatureEnabled(
34+
'IS_MICROSOFT_SYNC_ENABLED',
35+
);
2936

3037
return (
3138
<Card>
@@ -35,8 +42,16 @@ export const SettingsAccountsListEmptyStateCard = ({
3542
Icon={IconGoogle}
3643
title="Connect with Google"
3744
variant="secondary"
38-
onClick={handleOnClick}
45+
onClick={() => triggerApisOAuth('google')}
3946
/>
47+
{isMicrosoftSyncEnabled && (
48+
<Button
49+
Icon={IconMicrosoft}
50+
title="Connect with Microsoft"
51+
variant="secondary"
52+
onClick={() => triggerApisOAuth('microsoft')}
53+
/>
54+
)}
4055
</StyledBody>
4156
</Card>
4257
);

packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
1313
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
1414
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
15-
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
15+
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
1616
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
1717
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
1818
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@@ -35,8 +35,7 @@ export const SettingsAccountsRowDropdownMenu = ({
3535
const { destroyOneRecord } = useDestroyOneRecord({
3636
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
3737
});
38-
39-
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
38+
const { triggerApisOAuth } = useTriggerApisOAuth();
4039

4140
return (
4241
<Dropdown
@@ -71,7 +70,7 @@ export const SettingsAccountsRowDropdownMenu = ({
7170
LeftIcon={IconRefresh}
7271
text="Reconnect"
7372
onClick={() => {
74-
triggerGoogleApisOAuth();
73+
triggerApisOAuth(account.provider);
7574
closeDropdown();
7675
}}
7776
/>

packages/twenty-front/src/modules/settings/accounts/hooks/useTriggerGoogleApisOAuth.ts renamed to packages/twenty-front/src/modules/settings/accounts/hooks/useTriggerApiOAuth.ts

+29-15
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,35 @@ import {
88
useGenerateTransientTokenMutation,
99
} from '~/generated/graphql';
1010

11-
export const useTriggerGoogleApisOAuth = () => {
11+
const getProviderUrl = (provider: string) => {
12+
switch (provider) {
13+
case 'google':
14+
return 'google-apis';
15+
case 'microsoft':
16+
return 'microsoft-apis';
17+
default:
18+
throw new Error(`Provider ${provider} is not supported`);
19+
}
20+
};
21+
22+
export const useTriggerApisOAuth = () => {
1223
const [generateTransientToken] = useGenerateTransientTokenMutation();
1324

14-
const triggerGoogleApisOAuth = useCallback(
15-
async ({
16-
redirectLocation,
17-
messageVisibility,
18-
calendarVisibility,
19-
loginHint,
20-
}: {
21-
redirectLocation?: AppPath | string;
22-
messageVisibility?: MessageChannelVisibility;
23-
calendarVisibility?: CalendarChannelVisibility;
24-
loginHint?: string;
25-
} = {}) => {
25+
const triggerApisOAuth = useCallback(
26+
async (
27+
provider: string,
28+
{
29+
redirectLocation,
30+
messageVisibility,
31+
calendarVisibility,
32+
loginHint,
33+
}: {
34+
redirectLocation?: AppPath | string;
35+
messageVisibility?: MessageChannelVisibility;
36+
calendarVisibility?: CalendarChannelVisibility;
37+
loginHint?: string;
38+
} = {},
39+
) => {
2640
const authServerUrl = REACT_APP_SERVER_BASE_URL;
2741

2842
const transientToken = await generateTransientToken();
@@ -46,10 +60,10 @@ export const useTriggerGoogleApisOAuth = () => {
4660

4761
params += loginHint ? `&loginHint=${loginHint}` : '';
4862

49-
window.location.href = `${authServerUrl}/auth/google-apis?${params}`;
63+
window.location.href = `${authServerUrl}/auth/${getProviderUrl(provider)}?${params}`;
5064
},
5165
[generateTransientToken],
5266
);
5367

54-
return { triggerGoogleApisOAuth };
68+
return { triggerApisOAuth };
5569
};

packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope';
22
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
33
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
44
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
5-
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
5+
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
66
import { Select, SelectOption } from '@/ui/input/components/Select';
77
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
88
import { VariableTagInput } from '@/workflow/search-variables/components/VariableTagInput';
@@ -38,7 +38,8 @@ export const WorkflowEditActionFormSendEmail = (
3838
) => {
3939
const theme = useTheme();
4040
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
41-
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
41+
const { triggerApisOAuth } = useTriggerApisOAuth();
42+
4243
const workflowId = useRecoilValue(workflowIdState);
4344
const redirectUrl = `/object/workflow/${workflowId}`;
4445

@@ -66,7 +67,7 @@ export const WorkflowEditActionFormSendEmail = (
6667
!isDefined(scopes) ||
6768
!isDefined(scopes.find((scope) => scope === GMAIL_SEND_SCOPE))
6869
) {
69-
await triggerGoogleApisOAuth({
70+
await triggerApisOAuth('google', {
7071
redirectLocation: redirectUrl,
7172
loginHint: connectedAccount.handle,
7273
});
@@ -183,7 +184,7 @@ export const WorkflowEditActionFormSendEmail = (
183184
options={connectedAccountOptions}
184185
callToActionButton={{
185186
onClick: () =>
186-
triggerGoogleApisOAuth({ redirectLocation: redirectUrl }),
187+
triggerApisOAuth('google', { redirectLocation: redirectUrl }),
187188
Icon: IconPlus,
188189
text: 'Add account',
189190
}}

packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ export type FeatureFlagKey =
1616
| 'IS_SSO_ENABLED'
1717
| 'IS_UNIQUE_INDEXES_ENABLED'
1818
| 'IS_ARRAY_AND_JSON_FILTER_ENABLED'
19+
| 'IS_MICROSOFT_SYNC_ENABLED'
1920
| 'IS_ADVANCED_FILTERS_ENABLED';

packages/twenty-front/src/pages/onboarding/SyncEmails.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import { Title } from '@/auth/components/Title';
1010
import { currentUserState } from '@/auth/states/currentUserState';
1111
import { OnboardingSyncEmailsSettingsCard } from '@/onboarding/components/OnboardingSyncEmailsSettingsCard';
1212
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
13-
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
1413
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
1514
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
1615

16+
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
1717
import { AppPath } from '@/types/AppPath';
1818
import {
1919
CalendarChannelVisibility,
@@ -38,7 +38,7 @@ const StyledActionLinkContainer = styled.div`
3838

3939
export const SyncEmails = () => {
4040
const theme = useTheme();
41-
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
41+
const { triggerApisOAuth } = useTriggerApisOAuth();
4242
const setNextOnboardingStatus = useSetNextOnboardingStatus();
4343
const currentUser = useRecoilValue(currentUserState);
4444
const [visibility, setVisibility] = useState<MessageChannelVisibility>(
@@ -53,7 +53,7 @@ export const SyncEmails = () => {
5353
? CalendarChannelVisibility.ShareEverything
5454
: CalendarChannelVisibility.Metadata;
5555

56-
await triggerGoogleApisOAuth({
56+
await triggerApisOAuth('google', {
5757
redirectLocation: AppPath.Index,
5858
messageVisibility: visibility,
5959
calendarVisibility: calendarChannelVisibility,

packages/twenty-server/.env.example

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
2929
# AUTH_MICROSOFT_TENANT_ID=replace_me_with_azure_tenant_id
3030
# AUTH_MICROSOFT_CLIENT_SECRET=replace_me_with_azure_client_secret
3131
# AUTH_MICROSOFT_CALLBACK_URL=http://localhost:3000/auth/microsoft/redirect
32+
# AUTH_MICROSOFT_APIS_CALLBACK_URL=http://localhost:3000/auth/microsoft-apis/get-access-token
3233
# AUTH_GOOGLE_ENABLED=false
3334
# AUTH_GOOGLE_CLIENT_ID=replace_me_with_google_client_id
3435
# AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret

packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts

+5
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ export const seedFeatureFlags = async (
7575
workspaceId: workspaceId,
7676
value: false,
7777
},
78+
{
79+
key: FeatureFlagKey.IsMicrosoftSyncEnabled,
80+
workspaceId: workspaceId,
81+
value: true,
82+
},
7883
{
7984
key: FeatureFlagKey.IsAdvancedFiltersEnabled,
8085
workspaceId: workspaceId,

packages/twenty-server/src/engine/core-modules/auth/auth.module.ts

+4
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
88
import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
99
import { GoogleAPIsAuthController } from 'src/engine/core-modules/auth/controllers/google-apis-auth.controller';
1010
import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/google-auth.controller';
11+
import { MicrosoftAPIsAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller';
1112
import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
1213
import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller';
1314
import { VerifyAuthController } from 'src/engine/core-modules/auth/controllers/verify-auth.controller';
1415
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
1516
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
17+
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
1618
import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
1719
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
1820
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
@@ -80,6 +82,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
8082
GoogleAuthController,
8183
MicrosoftAuthController,
8284
GoogleAPIsAuthController,
85+
MicrosoftAPIsAuthController,
8386
VerifyAuthController,
8487
SSOAuthController,
8588
],
@@ -90,6 +93,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
9093
SamlAuthStrategy,
9194
AuthResolver,
9295
GoogleAPIsService,
96+
MicrosoftAPIsService,
9397
AppTokenService,
9498
AccessTokenService,
9599
LoginTokenService,

0 commit comments

Comments
 (0)