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 <raphael.bosi@gmail.com>
This commit is contained in:
brendanlaschke 2024-11-07 18:13:22 +01:00 committed by GitHub
parent 83f3963bfb
commit f9c076df31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 1417 additions and 118 deletions

View File

@ -23,6 +23,7 @@
"@linaria/core": "^6.2.0", "@linaria/core": "^6.2.0",
"@linaria/react": "^6.2.1", "@linaria/react": "^6.2.1",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@nestjs/apollo": "^11.0.5", "@nestjs/apollo": "^11.0.5",
"@nestjs/axios": "^3.0.1", "@nestjs/axios": "^3.0.1",
"@nestjs/cli": "^9.0.0", "@nestjs/cli": "^9.0.0",
@ -201,6 +202,7 @@
"@graphql-codegen/typescript": "^3.0.4", "@graphql-codegen/typescript": "^3.0.4",
"@graphql-codegen/typescript-operations": "^3.0.4", "@graphql-codegen/typescript-operations": "^3.0.4",
"@graphql-codegen/typescript-react-apollo": "^3.3.7", "@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@microsoft/microsoft-graph-types": "^2.40.0",
"@nestjs/cli": "^9.0.0", "@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0", "@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0", "@nestjs/testing": "^9.0.0",

View File

@ -1,7 +1,7 @@
import { InformationBanner } from '@/information-banner/components/InformationBanner'; import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect'; import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect';
import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys'; 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'; import { IconRefresh } from 'twenty-ui';
export const InformationBannerReconnectAccountEmailAliases = () => { export const InformationBannerReconnectAccountEmailAliases = () => {
@ -9,7 +9,7 @@ export const InformationBannerReconnectAccountEmailAliases = () => {
InformationBannerKeys.ACCOUNTS_TO_RECONNECT_EMAIL_ALIASES, InformationBannerKeys.ACCOUNTS_TO_RECONNECT_EMAIL_ALIASES,
); );
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth(); const { triggerApisOAuth } = useTriggerApisOAuth();
if (!accountToReconnect) { if (!accountToReconnect) {
return null; return null;
@ -20,7 +20,7 @@ export const InformationBannerReconnectAccountEmailAliases = () => {
message={`Please reconnect your mailbox ${accountToReconnect?.handle} to update your email aliases:`} message={`Please reconnect your mailbox ${accountToReconnect?.handle} to update your email aliases:`}
buttonTitle="Reconnect" buttonTitle="Reconnect"
buttonIcon={IconRefresh} buttonIcon={IconRefresh}
buttonOnClick={() => triggerGoogleApisOAuth()} buttonOnClick={() => triggerApisOAuth(accountToReconnect.provider)}
/> />
); );
}; };

View File

@ -1,7 +1,7 @@
import { InformationBanner } from '@/information-banner/components/InformationBanner'; import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect'; import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect';
import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys'; 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'; import { IconRefresh } from 'twenty-ui';
export const InformationBannerReconnectAccountInsufficientPermissions = () => { export const InformationBannerReconnectAccountInsufficientPermissions = () => {
@ -9,7 +9,7 @@ export const InformationBannerReconnectAccountInsufficientPermissions = () => {
InformationBannerKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS, InformationBannerKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS,
); );
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth(); const { triggerApisOAuth } = useTriggerApisOAuth();
if (!accountToReconnect) { if (!accountToReconnect) {
return null; return null;
@ -21,7 +21,7 @@ export const InformationBannerReconnectAccountInsufficientPermissions = () => {
reconnect for updates:`} reconnect for updates:`}
buttonTitle="Reconnect" buttonTitle="Reconnect"
buttonIcon={IconRefresh} buttonIcon={IconRefresh}
buttonOnClick={() => triggerGoogleApisOAuth()} buttonOnClick={() => triggerApisOAuth(accountToReconnect.provider)}
/> />
); );
}; };

View File

@ -1,5 +1,5 @@
import { useNavigate } from 'react-router-dom'; 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 { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/components/SettingsAccountsListEmptyStateCard'; import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/components/SettingsAccountsListEmptyStateCard';
@ -9,6 +9,11 @@ import { SettingsPath } from '@/types/SettingsPath';
import { SettingsAccountsConnectedAccountsRowRightContainer } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer'; import { SettingsAccountsConnectedAccountsRowRightContainer } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer';
import { SettingsListCard } from '../../components/SettingsListCard'; import { SettingsListCard } from '../../components/SettingsListCard';
const ProviderIcons: { [k: string]: IconComponent } = {
google: IconGoogle,
microsoft: IconMicrosoft,
};
export const SettingsAccountsConnectedAccountsListCard = ({ export const SettingsAccountsConnectedAccountsListCard = ({
accounts, accounts,
loading, loading,
@ -27,7 +32,7 @@ export const SettingsAccountsConnectedAccountsListCard = ({
items={accounts} items={accounts}
getItemLabel={(account) => account.handle} getItemLabel={(account) => account.handle}
isLoading={loading} isLoading={loading}
RowIcon={IconGoogle} RowIconFn={(row) => ProviderIcons[row.provider]}
RowRightComponent={({ item: account }) => ( RowRightComponent={({ item: account }) => (
<SettingsAccountsConnectedAccountsRowRightContainer account={account} /> <SettingsAccountsConnectedAccountsRowRightContainer account={account} />
)} )}

View File

@ -1,7 +1,14 @@
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Button, Card, CardContent, CardHeader, IconGoogle } from 'twenty-ui'; import {
Button,
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; Card,
CardContent,
CardHeader,
IconGoogle,
IconMicrosoft,
} from 'twenty-ui';
const StyledHeader = styled(CardHeader)` const StyledHeader = styled(CardHeader)`
align-items: center; align-items: center;
@ -12,6 +19,7 @@ const StyledHeader = styled(CardHeader)`
const StyledBody = styled(CardContent)` const StyledBody = styled(CardContent)`
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: ${({ theme }) => theme.spacing(2)};
`; `;
type SettingsAccountsListEmptyStateCardProps = { type SettingsAccountsListEmptyStateCardProps = {
@ -21,11 +29,10 @@ type SettingsAccountsListEmptyStateCardProps = {
export const SettingsAccountsListEmptyStateCard = ({ export const SettingsAccountsListEmptyStateCard = ({
label, label,
}: SettingsAccountsListEmptyStateCardProps) => { }: SettingsAccountsListEmptyStateCardProps) => {
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth(); const { triggerApisOAuth } = useTriggerApisOAuth();
const isMicrosoftSyncEnabled = useIsFeatureEnabled(
const handleOnClick = async () => { 'IS_MICROSOFT_SYNC_ENABLED',
await triggerGoogleApisOAuth(); );
};
return ( return (
<Card> <Card>
@ -35,8 +42,16 @@ export const SettingsAccountsListEmptyStateCard = ({
Icon={IconGoogle} Icon={IconGoogle}
title="Connect with Google" title="Connect with Google"
variant="secondary" variant="secondary"
onClick={handleOnClick} onClick={() => triggerApisOAuth('google')}
/> />
{isMicrosoftSyncEnabled && (
<Button
Icon={IconMicrosoft}
title="Connect with Microsoft"
variant="secondary"
onClick={() => triggerApisOAuth('microsoft')}
/>
)}
</StyledBody> </StyledBody>
</Card> </Card>
); );

View File

@ -12,7 +12,7 @@ import {
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord'; import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@ -35,8 +35,7 @@ export const SettingsAccountsRowDropdownMenu = ({
const { destroyOneRecord } = useDestroyOneRecord({ const { destroyOneRecord } = useDestroyOneRecord({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount, objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
}); });
const { triggerApisOAuth } = useTriggerApisOAuth();
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
return ( return (
<Dropdown <Dropdown
@ -71,7 +70,7 @@ export const SettingsAccountsRowDropdownMenu = ({
LeftIcon={IconRefresh} LeftIcon={IconRefresh}
text="Reconnect" text="Reconnect"
onClick={() => { onClick={() => {
triggerGoogleApisOAuth(); triggerApisOAuth(account.provider);
closeDropdown(); closeDropdown();
}} }}
/> />

View File

@ -8,11 +8,24 @@ import {
useGenerateTransientTokenMutation, useGenerateTransientTokenMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
export const useTriggerGoogleApisOAuth = () => { const getProviderUrl = (provider: string) => {
switch (provider) {
case 'google':
return 'google-apis';
case 'microsoft':
return 'microsoft-apis';
default:
throw new Error(`Provider ${provider} is not supported`);
}
};
export const useTriggerApisOAuth = () => {
const [generateTransientToken] = useGenerateTransientTokenMutation(); const [generateTransientToken] = useGenerateTransientTokenMutation();
const triggerGoogleApisOAuth = useCallback( const triggerApisOAuth = useCallback(
async ({ async (
provider: string,
{
redirectLocation, redirectLocation,
messageVisibility, messageVisibility,
calendarVisibility, calendarVisibility,
@ -22,7 +35,8 @@ export const useTriggerGoogleApisOAuth = () => {
messageVisibility?: MessageChannelVisibility; messageVisibility?: MessageChannelVisibility;
calendarVisibility?: CalendarChannelVisibility; calendarVisibility?: CalendarChannelVisibility;
loginHint?: string; loginHint?: string;
} = {}) => { } = {},
) => {
const authServerUrl = REACT_APP_SERVER_BASE_URL; const authServerUrl = REACT_APP_SERVER_BASE_URL;
const transientToken = await generateTransientToken(); const transientToken = await generateTransientToken();
@ -46,10 +60,10 @@ export const useTriggerGoogleApisOAuth = () => {
params += loginHint ? `&loginHint=${loginHint}` : ''; params += loginHint ? `&loginHint=${loginHint}` : '';
window.location.href = `${authServerUrl}/auth/google-apis?${params}`; window.location.href = `${authServerUrl}/auth/${getProviderUrl(provider)}?${params}`;
}, },
[generateTransientToken], [generateTransientToken],
); );
return { triggerGoogleApisOAuth }; return { triggerApisOAuth };
}; };

View File

@ -2,7 +2,7 @@ import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope';
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase'; import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
import { VariableTagInput } from '@/workflow/search-variables/components/VariableTagInput'; import { VariableTagInput } from '@/workflow/search-variables/components/VariableTagInput';
@ -38,7 +38,8 @@ export const WorkflowEditActionFormSendEmail = (
) => { ) => {
const theme = useTheme(); const theme = useTheme();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth(); const { triggerApisOAuth } = useTriggerApisOAuth();
const workflowId = useRecoilValue(workflowIdState); const workflowId = useRecoilValue(workflowIdState);
const redirectUrl = `/object/workflow/${workflowId}`; const redirectUrl = `/object/workflow/${workflowId}`;
@ -66,7 +67,7 @@ export const WorkflowEditActionFormSendEmail = (
!isDefined(scopes) || !isDefined(scopes) ||
!isDefined(scopes.find((scope) => scope === GMAIL_SEND_SCOPE)) !isDefined(scopes.find((scope) => scope === GMAIL_SEND_SCOPE))
) { ) {
await triggerGoogleApisOAuth({ await triggerApisOAuth('google', {
redirectLocation: redirectUrl, redirectLocation: redirectUrl,
loginHint: connectedAccount.handle, loginHint: connectedAccount.handle,
}); });
@ -183,7 +184,7 @@ export const WorkflowEditActionFormSendEmail = (
options={connectedAccountOptions} options={connectedAccountOptions}
callToActionButton={{ callToActionButton={{
onClick: () => onClick: () =>
triggerGoogleApisOAuth({ redirectLocation: redirectUrl }), triggerApisOAuth('google', { redirectLocation: redirectUrl }),
Icon: IconPlus, Icon: IconPlus,
text: 'Add account', text: 'Add account',
}} }}

View File

@ -16,4 +16,5 @@ export type FeatureFlagKey =
| 'IS_SSO_ENABLED' | 'IS_SSO_ENABLED'
| 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_UNIQUE_INDEXES_ENABLED'
| 'IS_ARRAY_AND_JSON_FILTER_ENABLED' | 'IS_ARRAY_AND_JSON_FILTER_ENABLED'
| 'IS_MICROSOFT_SYNC_ENABLED'
| 'IS_ADVANCED_FILTERS_ENABLED'; | 'IS_ADVANCED_FILTERS_ENABLED';

View File

@ -10,10 +10,10 @@ import { Title } from '@/auth/components/Title';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingSyncEmailsSettingsCard } from '@/onboarding/components/OnboardingSyncEmailsSettingsCard'; import { OnboardingSyncEmailsSettingsCard } from '@/onboarding/components/OnboardingSyncEmailsSettingsCard';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { import {
CalendarChannelVisibility, CalendarChannelVisibility,
@ -38,7 +38,7 @@ const StyledActionLinkContainer = styled.div`
export const SyncEmails = () => { export const SyncEmails = () => {
const theme = useTheme(); const theme = useTheme();
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth(); const { triggerApisOAuth } = useTriggerApisOAuth();
const setNextOnboardingStatus = useSetNextOnboardingStatus(); const setNextOnboardingStatus = useSetNextOnboardingStatus();
const currentUser = useRecoilValue(currentUserState); const currentUser = useRecoilValue(currentUserState);
const [visibility, setVisibility] = useState<MessageChannelVisibility>( const [visibility, setVisibility] = useState<MessageChannelVisibility>(
@ -53,7 +53,7 @@ export const SyncEmails = () => {
? CalendarChannelVisibility.ShareEverything ? CalendarChannelVisibility.ShareEverything
: CalendarChannelVisibility.Metadata; : CalendarChannelVisibility.Metadata;
await triggerGoogleApisOAuth({ await triggerApisOAuth('google', {
redirectLocation: AppPath.Index, redirectLocation: AppPath.Index,
messageVisibility: visibility, messageVisibility: visibility,
calendarVisibility: calendarChannelVisibility, calendarVisibility: calendarChannelVisibility,

View File

@ -29,6 +29,7 @@ ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
# AUTH_MICROSOFT_TENANT_ID=replace_me_with_azure_tenant_id # AUTH_MICROSOFT_TENANT_ID=replace_me_with_azure_tenant_id
# AUTH_MICROSOFT_CLIENT_SECRET=replace_me_with_azure_client_secret # AUTH_MICROSOFT_CLIENT_SECRET=replace_me_with_azure_client_secret
# AUTH_MICROSOFT_CALLBACK_URL=http://localhost:3000/auth/microsoft/redirect # AUTH_MICROSOFT_CALLBACK_URL=http://localhost:3000/auth/microsoft/redirect
# AUTH_MICROSOFT_APIS_CALLBACK_URL=http://localhost:3000/auth/microsoft-apis/get-access-token
# AUTH_GOOGLE_ENABLED=false # AUTH_GOOGLE_ENABLED=false
# AUTH_GOOGLE_CLIENT_ID=replace_me_with_google_client_id # AUTH_GOOGLE_CLIENT_ID=replace_me_with_google_client_id
# AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret # AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret

View File

@ -75,6 +75,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId, workspaceId: workspaceId,
value: false, value: false,
}, },
{
key: FeatureFlagKey.IsMicrosoftSyncEnabled,
workspaceId: workspaceId,
value: true,
},
{ {
key: FeatureFlagKey.IsAdvancedFiltersEnabled, key: FeatureFlagKey.IsAdvancedFiltersEnabled,
workspaceId: workspaceId, workspaceId: workspaceId,

View File

@ -8,11 +8,13 @@ import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service'; import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
import { GoogleAPIsAuthController } from 'src/engine/core-modules/auth/controllers/google-apis-auth.controller'; import { GoogleAPIsAuthController } from 'src/engine/core-modules/auth/controllers/google-apis-auth.controller';
import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/google-auth.controller'; import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/google-auth.controller';
import { MicrosoftAPIsAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller';
import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller'; import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller'; import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller';
import { VerifyAuthController } from 'src/engine/core-modules/auth/controllers/verify-auth.controller'; import { VerifyAuthController } from 'src/engine/core-modules/auth/controllers/verify-auth.controller';
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service'; import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service'; import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service'; import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service'; import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service'; import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
@ -80,6 +82,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
GoogleAuthController, GoogleAuthController,
MicrosoftAuthController, MicrosoftAuthController,
GoogleAPIsAuthController, GoogleAPIsAuthController,
MicrosoftAPIsAuthController,
VerifyAuthController, VerifyAuthController,
SSOAuthController, SSOAuthController,
], ],
@ -90,6 +93,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
SamlAuthStrategy, SamlAuthStrategy,
AuthResolver, AuthResolver,
GoogleAPIsService, GoogleAPIsService,
MicrosoftAPIsService,
AppTokenService, AppTokenService,
AccessTokenService, AccessTokenService,
LoginTokenService, LoginTokenService,

View File

@ -0,0 +1,105 @@
import {
Controller,
Get,
Req,
Res,
UseFilters,
UseGuards,
} from '@nestjs/common';
import { Response } from 'express';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
import { MicrosoftAPIsOauthExchangeCodeForTokenGuard } from 'src/engine/core-modules/auth/guards/microsoft-apis-oauth-exchange-code-for-token.guard';
import { MicrosoftAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth/guards/mircosoft-apis-oauth-request-code.guard';
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microsoft-api-request.type';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
@Controller('auth/microsoft-apis')
@UseFilters(AuthRestApiExceptionFilter)
export class MicrosoftAPIsAuthController {
constructor(
private readonly microsoftAPIsService: MicrosoftAPIsService,
private readonly transientTokenService: TransientTokenService,
private readonly environmentService: EnvironmentService,
private readonly onboardingService: OnboardingService,
) {}
@Get()
@UseGuards(MicrosoftAPIsOauthRequestCodeGuard)
async MicrosoftAuth() {
// As this method is protected by Microsoft Auth guard, it will trigger Microsoft SSO flow
return;
}
@Get('get-access-token')
@UseGuards(MicrosoftAPIsOauthExchangeCodeForTokenGuard)
async MicrosoftAuthGetAccessToken(
@Req() req: MicrosoftAPIsRequest,
@Res() res: Response,
) {
const { user } = req;
const {
emails,
accessToken,
refreshToken,
transientToken,
redirectLocation,
calendarVisibility,
messageVisibility,
} = user;
const { workspaceMemberId, userId, workspaceId } =
await this.transientTokenService.verifyTransientToken(transientToken);
const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS');
if (demoWorkspaceIds.includes(workspaceId)) {
throw new AuthException(
'Cannot connect Microsoft account to demo workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}
if (!workspaceId) {
throw new AuthException(
'Workspace not found',
AuthExceptionCode.WORKSPACE_NOT_FOUND,
);
}
const handle = emails[0].value;
await this.microsoftAPIsService.refreshMicrosoftRefreshToken({
handle,
workspaceMemberId: workspaceMemberId,
workspaceId: workspaceId,
accessToken,
refreshToken,
calendarVisibility,
messageVisibility,
});
if (userId) {
await this.onboardingService.setOnboardingConnectAccountPending({
userId,
workspaceId,
value: false,
});
}
return res.redirect(
`${this.environmentService.get('FRONT_BASE_URL')}${
redirectLocation || '/settings/accounts'
}`,
);
}
}

View File

@ -0,0 +1,31 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { MicrosoftAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/microsoft-apis-oauth-exchange-code-for-token.auth.strategy';
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Injectable()
export class MicrosoftAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
'microsoft-apis',
) {
constructor(private readonly environmentService: EnvironmentService) {
super();
}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const state = JSON.parse(request.query.state);
new MicrosoftAPIsOauthExchangeCodeForTokenStrategy(this.environmentService);
setRequestExtraParams(request, {
transientToken: state.transientToken,
redirectLocation: state.redirectLocation,
calendarVisibility: state.calendarVisibility,
messageVisibility: state.messageVisibility,
});
return (await super.canActivate(context)) as boolean;
}
}

View File

@ -0,0 +1,62 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { MicrosoftAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth/strategies/microsoft-apis-oauth-request-code.auth.strategy';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard(
'microsoft-apis',
) {
constructor(
private readonly environmentService: EnvironmentService,
private readonly featureFlagService: FeatureFlagService,
private readonly transientTokenService: TransientTokenService,
) {
super({
prompt: 'select_account',
});
}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const { workspaceId } =
await this.transientTokenService.verifyTransientToken(
request.query.transientToken,
);
const isMicrosoftSyncEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsMicrosoftSyncEnabled,
workspaceId,
);
if (!isMicrosoftSyncEnabled) {
throw new AuthException(
'Microsoft sync is not enabled',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}
new MicrosoftAPIsOauthRequestCodeStrategy(this.environmentService);
setRequestExtraParams(request, {
transientToken: request.query.transientToken,
redirectLocation: request.query.redirectLocation,
calendarVisibility: request.query.calendarVisibility,
messageVisibility: request.query.messageVisibility,
loginHint: request.query.loginHint,
});
const activate = (await super.canActivate(context)) as boolean;
return activate;
}
}

View File

@ -3,14 +3,17 @@ import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm'; import { EntityManager } from 'typeorm';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { getGoogleApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { import {
CalendarEventListFetchJob, CalendarEventListFetchJob,
CalendarEventsImportJobData, CalendarEventListFetchJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job'; } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import { import {
CalendarChannelVisibility, CalendarChannelVisibility,
@ -33,9 +36,6 @@ import {
MessagingMessageListFetchJobData, MessagingMessageListFetchJobData,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job'; } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
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';
import { getGoogleApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
@Injectable() @Injectable()
export class GoogleAPIsService { export class GoogleAPIsService {
@ -222,7 +222,7 @@ export class GoogleAPIsService {
}); });
for (const calendarChannel of calendarChannels) { for (const calendarChannel of calendarChannels) {
await this.calendarQueueService.add<CalendarEventsImportJobData>( await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name, CalendarEventListFetchJob.name,
{ {
calendarChannelId: calendarChannel.id, calendarChannelId: calendarChannel.id,

View File

@ -0,0 +1,212 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { v4 } from 'uuid';
import { getMicrosoftApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
CalendarEventListFetchJob,
CalendarEventListFetchJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import {
CalendarChannelVisibility,
CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import {
ConnectedAccountProvider,
ConnectedAccountWorkspaceEntity,
} from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
MessageChannelType,
MessageChannelVisibility,
MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class MicrosoftAPIsService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectMessageQueue(MessageQueue.calendarQueue)
private readonly calendarQueueService: MessageQueueService,
private readonly accountsToReconnectService: AccountsToReconnectService,
) {}
async refreshMicrosoftRefreshToken(input: {
handle: string;
workspaceMemberId: string;
workspaceId: string;
accessToken: string;
refreshToken: string;
calendarVisibility: CalendarChannelVisibility | undefined;
messageVisibility: MessageChannelVisibility | undefined;
}) {
const {
handle,
workspaceId,
workspaceMemberId,
calendarVisibility,
messageVisibility,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { handle, accountOwnerId: workspaceMemberId },
});
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
const calendarChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(workspaceId);
const scopes = getMicrosoftApisOauthScopes();
await workspaceDataSource.transaction(async (manager: EntityManager) => {
if (!existingAccountId) {
await connectedAccountRepository.save(
{
id: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.MICROSOFT,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
},
{},
manager,
);
// TODO: Modify this when the email sync is implemented
await messageChannelRepository.save(
{
id: v4(),
connectedAccountId: newOrExistingConnectedAccountId,
type: MessageChannelType.EMAIL,
handle,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.FAILED,
},
{},
manager,
);
await calendarChannelRepository.save(
{
id: v4(),
connectedAccountId: newOrExistingConnectedAccountId,
handle,
visibility:
calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING,
},
{},
manager,
);
} else {
await connectedAccountRepository.update(
{
id: newOrExistingConnectedAccountId,
},
{
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
},
manager,
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: { id: workspaceMemberId },
});
const userId = workspaceMember.userId;
await this.accountsToReconnectService.removeAccountToReconnect(
userId,
workspaceId,
newOrExistingConnectedAccountId,
);
// TODO: Modify this when the email sync is implemented
await messageChannelRepository.update(
{
connectedAccountId: newOrExistingConnectedAccountId,
},
{
syncStage: MessageChannelSyncStage.FAILED,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncCursor: '',
syncStageStartedAt: null,
},
manager,
);
}
});
// TODO: Uncomment this when the email sync is implemented
// const messageChannels = await messageChannelRepository.find({
// where: {
// connectedAccountId: newOrExistingConnectedAccountId,
// },
// });
// for (const messageChannel of messageChannels) {
// await this.messageQueueService.add<MessagingMessageListFetchJobData>(
// MessagingMessageListFetchJob.name,
// {
// workspaceId,
// messageChannelId: messageChannel.id,
// },
// );
// }
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const calendarChannel of calendarChannels) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
}
}
}

View File

@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-microsoft';
import { getMicrosoftApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
export type MicrosoftAPIScopeConfig = {
isCalendarEnabled?: boolean;
isMessagingAliasFetchingEnabled?: boolean;
};
@Injectable()
export class MicrosoftAPIsOauthCommonStrategy extends PassportStrategy(
Strategy,
'microsoft-apis',
) {
constructor(environmentService: EnvironmentService) {
const scopes = getMicrosoftApisOauthScopes();
super({
clientID: environmentService.get('AUTH_MICROSOFT_CLIENT_ID'),
clientSecret: environmentService.get('AUTH_MICROSOFT_CLIENT_SECRET'),
tenant: environmentService.get('AUTH_MICROSOFT_TENANT_ID'),
callbackURL: environmentService.get('AUTH_MICROSOFT_APIS_CALLBACK_URL'),
scope: scopes,
passReqToCallback: true,
});
}
}

View File

@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import { VerifyCallback } from 'passport-google-oauth20';
import { MicrosoftAPIsOauthCommonStrategy } from 'src/engine/core-modules/auth/strategies/microsoft-apis-oauth-common.auth.strategy';
import { MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microsoft-api-request.type';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
export type MicrosoftAPIScopeConfig = {
isCalendarEnabled?: boolean;
};
@Injectable()
export class MicrosoftAPIsOauthExchangeCodeForTokenStrategy extends MicrosoftAPIsOauthCommonStrategy {
constructor(environmentService: EnvironmentService) {
super(environmentService);
}
async validate(
request: MicrosoftAPIsRequest,
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<void> {
const { name, emails, photos } = profile;
const state =
typeof request.query.state === 'string'
? JSON.parse(request.query.state)
: undefined;
const user: MicrosoftAPIsRequest['user'] = {
emails,
firstName: name.givenName,
lastName: name.familyName,
picture: photos?.[0]?.value,
accessToken,
refreshToken,
transientToken: state.transientToken,
redirectLocation: state.redirectLocation,
calendarVisibility: state.calendarVisibility,
messageVisibility: state.messageVisibility,
};
done(null, user);
}
}

View File

@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { MicrosoftAPIsOauthCommonStrategy } from 'src/engine/core-modules/auth/strategies/microsoft-apis-oauth-common.auth.strategy';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Injectable()
export class MicrosoftAPIsOauthRequestCodeStrategy extends MicrosoftAPIsOauthCommonStrategy {
constructor(environmentService: EnvironmentService) {
super(environmentService);
}
authenticate(req: any, options: any) {
options = {
...options,
accessType: 'offline',
prompt: 'consent',
loginHint: req.params.loginHint,
state: JSON.stringify({
transientToken: req.params.transientToken,
redirectLocation: req.params.redirectLocation,
calendarVisibility: req.params.calendarVisibility,
messageVisibility: req.params.messageVisibility,
}),
};
return super.authenticate(req, options);
}
}

View File

@ -0,0 +1,23 @@
import { Request } from 'express';
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
export type MicrosoftAPIsRequest = Omit<
Request,
'user' | 'workspace' | 'workspaceMetadataVersion'
> & {
user: {
firstName?: string | null;
lastName?: string | null;
emails: { value: string }[];
picture: string | null;
workspaceInviteHash?: string;
accessToken: string;
refreshToken: string;
transientToken: string;
redirectLocation?: string;
calendarVisibility?: CalendarChannelVisibility;
messageVisibility?: MessageChannelVisibility;
};
};

View File

@ -0,0 +1,12 @@
export const getMicrosoftApisOauthScopes = () => {
const scopes = [
'openid',
'email',
'profile',
'offline_access',
'Mail.Read',
'Calendars.Read',
];
return scopes;
};

View File

@ -201,6 +201,10 @@ export class EnvironmentVariables {
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED) @ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
AUTH_MICROSOFT_CALLBACK_URL: string; AUTH_MICROSOFT_CALLBACK_URL: string;
@IsUrl({ require_tld: false })
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
AUTH_MICROSOFT_APIS_CALLBACK_URL: string;
@CastToBoolean() @CastToBoolean()
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()

View File

@ -14,5 +14,6 @@ export enum FeatureFlagKey {
IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED', IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED',
IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED', IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED',
IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED', IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED',
IsMicrosoftSyncEnabled = 'IS_MICROSOFT_SYNC_ENABLED',
IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED', IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED',
} }

View File

@ -35,7 +35,6 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard'; import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { streamToBuffer } from 'src/utils/stream-to-buffer'; import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { isDefined } from 'src/utils/is-defined';
const getHMACKey = (email?: string, key?: string | null) => { const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null; if (!email || !key) return null;

View File

@ -10,14 +10,19 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
import { CalendarEventCleanerModule } from 'src/modules/calendar/calendar-event-cleaner/calendar-event-cleaner.module'; import { CalendarEventCleanerModule } from 'src/modules/calendar/calendar-event-cleaner/calendar-event-cleaner.module';
import { CalendarEventListFetchCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-event-list-fetch.cron.command'; import { CalendarEventListFetchCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-event-list-fetch.cron.command';
import { CalendarEventsImportCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-import.cron.command';
import { CalendarOngoingStaleCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-ongoing-stale.cron.command'; import { CalendarOngoingStaleCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-ongoing-stale.cron.command';
import { CalendarEventListFetchCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job'; import { CalendarEventListFetchCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job';
import { CalendarEventsImportCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job';
import { CalendarOngoingStaleCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-ongoing-stale.cron.job'; import { CalendarOngoingStaleCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-ongoing-stale.cron.job';
import { GoogleCalendarDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module'; import { GoogleCalendarDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module';
import { MicrosoftCalendarDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/microsoft-calendar-driver.module';
import { CalendarEventListFetchJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job'; import { CalendarEventListFetchJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import { CalendarEventsImportJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job';
import { CalendarOngoingStaleJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job'; import { CalendarOngoingStaleJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job';
import { CalendarEventImportErrorHandlerService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service'; import { CalendarEventImportErrorHandlerService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service';
import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service'; import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service';
import { CalendarFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service';
import { CalendarGetCalendarEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service'; import { CalendarGetCalendarEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service';
import { CalendarSaveEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service'; import { CalendarSaveEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service';
import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module'; import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module';
@ -39,6 +44,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
WorkspaceDataSourceModule, WorkspaceDataSourceModule,
CalendarEventCleanerModule, CalendarEventCleanerModule,
GoogleCalendarDriverModule, GoogleCalendarDriverModule,
MicrosoftCalendarDriverModule,
BillingModule, BillingModule,
RefreshAccessTokenManagerModule, RefreshAccessTokenManagerModule,
CalendarEventParticipantManagerModule, CalendarEventParticipantManagerModule,
@ -48,16 +54,20 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
providers: [ providers: [
CalendarChannelSyncStatusService, CalendarChannelSyncStatusService,
CalendarEventsImportService, CalendarEventsImportService,
CalendarFetchEventsService,
CalendarEventImportErrorHandlerService, CalendarEventImportErrorHandlerService,
CalendarGetCalendarEventsService, CalendarGetCalendarEventsService,
CalendarSaveEventsService, CalendarSaveEventsService,
CalendarEventListFetchCronJob, CalendarEventListFetchCronJob,
CalendarEventListFetchCronCommand, CalendarEventListFetchCronCommand,
CalendarEventListFetchJob, CalendarEventListFetchJob,
CalendarEventsImportCronJob,
CalendarEventsImportCronCommand,
CalendarEventsImportJob,
CalendarOngoingStaleCronJob, CalendarOngoingStaleCronJob,
CalendarOngoingStaleCronCommand, CalendarOngoingStaleCronCommand,
CalendarOngoingStaleJob, CalendarOngoingStaleJob,
], ],
exports: [CalendarEventsImportService], exports: [CalendarEventsImportService, CalendarFetchEventsService],
}) })
export class CalendarEventImportManagerModule {} export class CalendarEventImportManagerModule {}

View File

@ -0,0 +1 @@
export const CALENDAR_EVENT_IMPORT_BATCH_SIZE = 100;

View File

@ -3,10 +3,8 @@ import { Command, CommandRunner } from 'nest-commander';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { import { CalendarEventListFetchCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job';
CALENDAR_EVENTS_IMPORT_CRON_PATTERN, import { CALENDAR_EVENTS_IMPORT_CRON_PATTERN } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job';
CalendarEventListFetchCronJob,
} from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job';
@Command({ @Command({
name: 'cron:calendar:calendar-event-list-fetch', name: 'cron:calendar:calendar-event-list-fetch',

View File

@ -0,0 +1,32 @@
import { Command, CommandRunner } from 'nest-commander';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import {
CALENDAR_EVENTS_IMPORT_CRON_PATTERN,
CalendarEventsImportCronJob,
} from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job';
@Command({
name: 'cron:calendar:calendar-events-import',
description: 'Starts a cron job to import the calendar events',
})
export class CalendarEventsImportCronCommand extends CommandRunner {
constructor(
@InjectMessageQueue(MessageQueue.cronQueue)
private readonly messageQueueService: MessageQueueService,
) {
super();
}
async run(): Promise<void> {
await this.messageQueueService.addCron<undefined>(
CalendarEventsImportCronJob.name,
undefined,
{
repeat: { pattern: CALENDAR_EVENTS_IMPORT_CRON_PATTERN },
},
);
}
}

View File

@ -16,11 +16,11 @@ import {
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { import {
CalendarEventListFetchJob, CalendarEventListFetchJob,
CalendarEventsImportJobData, CalendarEventListFetchJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job'; } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
export const CALENDAR_EVENTS_IMPORT_CRON_PATTERN = '*/5 * * * *'; export const CALENDAR_EVENT_LIST_FETCH_CRON_PATTERN = '*/5 * * * *';
@Processor({ @Processor({
queueName: MessageQueue.cronQueue, queueName: MessageQueue.cronQueue,
@ -38,7 +38,7 @@ export class CalendarEventListFetchCronJob {
@Process(CalendarEventListFetchCronJob.name) @Process(CalendarEventListFetchCronJob.name)
@SentryCronMonitor( @SentryCronMonitor(
CalendarEventListFetchCronJob.name, CalendarEventListFetchCronJob.name,
CALENDAR_EVENTS_IMPORT_CRON_PATTERN, CALENDAR_EVENT_LIST_FETCH_CRON_PATTERN,
) )
async handle(): Promise<void> { async handle(): Promise<void> {
console.time('CalendarEventListFetchCronJob time'); console.time('CalendarEventListFetchCronJob time');
@ -68,7 +68,7 @@ export class CalendarEventListFetchCronJob {
}); });
for (const calendarChannel of calendarChannels) { for (const calendarChannel of calendarChannels) {
await this.messageQueueService.add<CalendarEventsImportJobData>( await this.messageQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name, CalendarEventListFetchJob.name,
{ {
calendarChannelId: calendarChannel.id, calendarChannelId: calendarChannel.id,

View File

@ -0,0 +1,87 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Equal, Repository } from 'typeorm';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import {
Workspace,
WorkspaceActivationStatus,
} from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { CalendarEventListFetchJobData } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import { CalendarEventsImportJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job';
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
export const CALENDAR_EVENTS_IMPORT_CRON_PATTERN = '*/1 * * * *';
@Processor({
queueName: MessageQueue.cronQueue,
})
export class CalendarEventsImportCronJob {
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectMessageQueue(MessageQueue.calendarQueue)
private readonly messageQueueService: MessageQueueService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
@Process(CalendarEventsImportCronJob.name)
@SentryCronMonitor(
CalendarEventsImportCronJob.name,
CALENDAR_EVENTS_IMPORT_CRON_PATTERN,
)
async handle(): Promise<void> {
console.time('CalendarEventsImportCronJob time');
const activeWorkspaces = await this.workspaceRepository.find({
where: {
activationStatus: WorkspaceActivationStatus.ACTIVE,
},
});
for (const activeWorkspace of activeWorkspaces) {
try {
const calendarChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
activeWorkspace.id,
'calendarChannel',
);
const calendarChannels = await calendarChannelRepository.find({
where: {
isSyncEnabled: true,
syncStage: Equal(
CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING,
),
},
});
for (const calendarChannel of calendarChannels) {
await this.messageQueueService.add<CalendarEventListFetchJobData>(
CalendarEventsImportJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId: activeWorkspace.id,
},
);
}
} catch (error) {
this.exceptionHandlerService.captureExceptions([error], {
user: {
workspaceId: activeWorkspace.id,
},
});
}
}
console.timeEnd('CalendarEventsImportCronJob time');
}
}

View File

@ -72,6 +72,7 @@ export class GoogleCalendarGetEventsService {
} }
return { return {
fullEvents: true,
calendarEvents: formatGoogleCalendarEvents(events), calendarEvents: formatGoogleCalendarEvents(events),
nextSyncCursor: nextSyncToken || '', nextSyncCursor: nextSyncToken || '',
}; };

View File

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
import { MicrosoftCalendarGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-get-events.service';
import { MicrosoftCalendarImportEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
@Module({
imports: [EnvironmentModule],
providers: [
MicrosoftCalendarGetEventsService,
MicrosoftCalendarImportEventsService,
MicrosoftOAuth2ClientManagerService,
],
exports: [
MicrosoftCalendarGetEventsService,
MicrosoftCalendarImportEventsService,
],
})
export class MicrosoftCalendarDriverModule {}

View File

@ -0,0 +1,62 @@
import { Injectable } from '@nestjs/common';
import {
PageCollection,
PageIterator,
PageIteratorCallback,
} from '@microsoft/microsoft-graph-client';
import { parseMicrosoftCalendarError } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util';
import { GetCalendarEventsResponse } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Injectable()
export class MicrosoftCalendarGetEventsService {
constructor(
private readonly microsoftOAuth2ClientManagerService: MicrosoftOAuth2ClientManagerService,
) {}
public async getCalendarEvents(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken' | 'id'
>,
syncCursor?: string,
): Promise<GetCalendarEventsResponse> {
try {
const microsoftClient =
await this.microsoftOAuth2ClientManagerService.getOAuth2Client(
connectedAccount.refreshToken,
);
const eventIds: string[] = [];
const response: PageCollection = await microsoftClient
.api(syncCursor || '/me/calendar/events/delta')
.version('beta')
.get();
const callback: PageIteratorCallback = (data) => {
eventIds.push(data.id);
return true;
};
const pageIterator = new PageIterator(
microsoftClient,
response,
callback,
);
await pageIterator.iterate();
return {
fullEvents: false,
calendarEventIds: eventIds,
nextSyncCursor: pageIterator.getDeltaLink() || '',
};
} catch (error) {
throw parseMicrosoftCalendarError(error);
}
}
}

View File

@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { Event } from '@microsoft/microsoft-graph-types';
import { formatMicrosoftCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util';
import { parseMicrosoftCalendarError } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util';
import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Injectable()
export class MicrosoftCalendarImportEventsService {
constructor(
private readonly microsoftOAuth2ClientManagerService: MicrosoftOAuth2ClientManagerService,
) {}
public async getCalendarEvents(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken' | 'id'
>,
changedEventIds: string[],
): Promise<CalendarEventWithParticipants[]> {
try {
const microsoftClient =
await this.microsoftOAuth2ClientManagerService.getOAuth2Client(
connectedAccount.refreshToken,
);
const events: Event[] = [];
for (const changedEventId of changedEventIds) {
const event = await microsoftClient
.api(`/me/calendar/events/${changedEventId}`)
.get();
events.push(event);
}
return formatMicrosoftCalendarEvents(events);
} catch (error) {
throw parseMicrosoftCalendarError(error);
}
}
}

View File

@ -0,0 +1,60 @@
import {
Event,
NullableOption,
ResponseType,
} from '@microsoft/microsoft-graph-types';
import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event';
export const formatMicrosoftCalendarEvents = (
events: Event[],
): CalendarEventWithParticipants[] => {
return events.map(formatMicrosoftCalendarEvent);
};
const formatMicrosoftCalendarEvent = (
event: Event,
): CalendarEventWithParticipants => {
const formatResponseStatus = (
status: NullableOption<ResponseType> | undefined,
) => {
switch (status) {
case 'accepted':
case 'organizer':
return CalendarEventParticipantResponseStatus.ACCEPTED;
case 'declined':
return CalendarEventParticipantResponseStatus.DECLINED;
case 'tentativelyAccepted':
return CalendarEventParticipantResponseStatus.TENTATIVE;
default:
return CalendarEventParticipantResponseStatus.NEEDS_ACTION;
}
};
return {
title: event.subject ?? '',
isCanceled: !!event.isCancelled,
isFullDay: !!event.isAllDay,
startsAt: event.start?.dateTime ?? null,
endsAt: event.end?.dateTime ?? null,
externalId: event.id ?? '',
externalCreatedAt: event.createdDateTime ?? null,
externalUpdatedAt: event.lastModifiedDateTime ?? null,
description: event.body?.content ?? '',
location: event.location?.displayName ?? '',
iCalUID: event.iCalUId ?? '',
conferenceSolution: event.onlineMeetingProvider ?? '',
conferenceLinkLabel: event.onlineMeeting?.joinUrl ?? '',
conferenceLinkUrl: event.onlineMeeting?.joinUrl ?? '',
recurringEventExternalId: event.id ?? '',
participants:
event.attendees?.map((attendee) => ({
handle: attendee.emailAddress?.address ?? '',
displayName: attendee.emailAddress?.name ?? '',
isOrganizer: attendee.status?.response === 'organizer',
responseStatus: formatResponseStatus(attendee.status?.response),
})) ?? [],
status: '',
};
};

View File

@ -0,0 +1,65 @@
import { GraphError } from '@microsoft/microsoft-graph-client';
import {
CalendarEventImportDriverException,
CalendarEventImportDriverExceptionCode,
} from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception';
export const parseMicrosoftCalendarError = (
error: GraphError,
): CalendarEventImportDriverException => {
const { statusCode, message } = error;
switch (statusCode) {
case 400:
return new CalendarEventImportDriverException(
message,
CalendarEventImportDriverExceptionCode.UNKNOWN,
);
case 404:
if (
message ==
'The mailbox is either inactive, soft-deleted, or is hosted on-premise.'
) {
return new CalendarEventImportDriverException(
message,
CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
}
return new CalendarEventImportDriverException(
message,
CalendarEventImportDriverExceptionCode.NOT_FOUND,
);
case 429:
return new CalendarEventImportDriverException(
message,
CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR,
);
case 403:
return new CalendarEventImportDriverException(
message,
CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
case 401:
return new CalendarEventImportDriverException(
message,
CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
case 500:
return new CalendarEventImportDriverException(
message,
CalendarEventImportDriverExceptionCode.UNKNOWN,
);
default:
return new CalendarEventImportDriverException(
message,
CalendarEventImportDriverExceptionCode.UNKNOWN,
);
}
};

View File

@ -4,14 +4,14 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service'; import { CalendarFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service';
import { import {
CalendarChannelSyncStage, CalendarChannelSyncStage,
CalendarChannelWorkspaceEntity, CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; import { isThrottled } from 'src/modules/connected-account/utils/is-throttled';
export type CalendarEventsImportJobData = { export type CalendarEventListFetchJobData = {
calendarChannelId: string; calendarChannelId: string;
workspaceId: string; workspaceId: string;
}; };
@ -23,11 +23,11 @@ export type CalendarEventsImportJobData = {
export class CalendarEventListFetchJob { export class CalendarEventListFetchJob {
constructor( constructor(
private readonly twentyORMManager: TwentyORMManager, private readonly twentyORMManager: TwentyORMManager,
private readonly calendarEventsImportService: CalendarEventsImportService, private readonly calendarFetchEventsService: CalendarFetchEventsService,
) {} ) {}
@Process(CalendarEventListFetchJob.name) @Process(CalendarEventListFetchJob.name)
async handle(data: CalendarEventsImportJobData): Promise<void> { async handle(data: CalendarEventListFetchJobData): Promise<void> {
console.time('CalendarEventListFetchJob time'); console.time('CalendarEventListFetchJob time');
const { workspaceId, calendarChannelId } = data; const { workspaceId, calendarChannelId } = data;
@ -65,7 +65,7 @@ export class CalendarEventListFetchJob {
syncStageStartedAt: null, syncStageStartedAt: null,
}); });
await this.calendarEventsImportService.processCalendarEventsImport( await this.calendarFetchEventsService.fetchCalendarEvents(
calendarChannel, calendarChannel,
calendarChannel.connectedAccount, calendarChannel.connectedAccount,
workspaceId, workspaceId,
@ -73,7 +73,7 @@ export class CalendarEventListFetchJob {
break; break;
case CalendarChannelSyncStage.PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING: case CalendarChannelSyncStage.PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING:
await this.calendarEventsImportService.processCalendarEventsImport( await this.calendarFetchEventsService.fetchCalendarEvents(
calendarChannel, calendarChannel,
calendarChannel.connectedAccount, calendarChannel.connectedAccount,
workspaceId, workspaceId,

View File

@ -0,0 +1,75 @@
import { Scope } from '@nestjs/common';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service';
import {
CalendarChannelSyncStage,
CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { isThrottled } from 'src/modules/connected-account/utils/is-throttled';
export type CalendarEventsImportJobData = {
calendarChannelId: string;
workspaceId: string;
};
@Processor({
queueName: MessageQueue.calendarQueue,
scope: Scope.REQUEST,
})
export class CalendarEventsImportJob {
constructor(
private readonly calendarEventsImportService: CalendarEventsImportService,
private readonly twentyORMManager: TwentyORMManager,
) {}
@Process(CalendarEventsImportJob.name)
async handle(data: CalendarEventsImportJobData): Promise<void> {
console.time('CalendarEventsImportJob time');
const { calendarChannelId, workspaceId } = data;
const calendarChannelRepository =
await this.twentyORMManager.getRepository<CalendarChannelWorkspaceEntity>(
'calendarChannel',
);
const calendarChannel = await calendarChannelRepository.findOne({
where: {
id: calendarChannelId,
isSyncEnabled: true,
},
relations: ['connectedAccount'],
});
if (!calendarChannel?.isSyncEnabled) {
return;
}
if (
isThrottled(
calendarChannel.syncStageStartedAt,
calendarChannel.throttleFailureCount,
)
) {
return;
}
if (
calendarChannel.syncStage !==
CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING
) {
return;
}
await this.calendarEventsImportService.processCalendarEventsImport(
calendarChannel,
calendarChannel.connectedAccount,
workspaceId,
);
console.timeEnd('CalendarEventsImportJob time');
}
}

View File

@ -2,84 +2,86 @@ import { Injectable } from '@nestjs/common';
import { Any } from 'typeorm'; import { Any } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklist.repository'; import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklist.repository';
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
import { CalendarEventCleanerService } from 'src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service'; import { CalendarEventCleanerService } from 'src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service';
import { CALENDAR_EVENT_IMPORT_BATCH_SIZE } from 'src/modules/calendar/calendar-event-import-manager/constants/calendar-event-import-batch-size';
import { MicrosoftCalendarImportEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service';
import { import {
CalendarEventImportErrorHandlerService, CalendarEventImportErrorHandlerService,
CalendarEventImportSyncStep, CalendarEventImportSyncStep,
} from 'src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service'; } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service';
import {
CalendarGetCalendarEventsService,
GetCalendarEventsResponse,
} from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service';
import { CalendarSaveEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service'; import { CalendarSaveEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service';
import { filterEventsAndReturnCancelledEvents } from 'src/modules/calendar/calendar-event-import-manager/utils/filter-events.util'; import { filterEventsAndReturnCancelledEvents } from 'src/modules/calendar/calendar-event-import-manager/utils/filter-events.util';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
import { import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
CalendarChannelSyncStage, import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event';
CalendarChannelWorkspaceEntity,
} 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 { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Injectable() @Injectable()
export class CalendarEventsImportService { export class CalendarEventsImportService {
constructor( constructor(
@InjectCacheStorage(CacheStorageNamespace.ModuleCalendar)
private readonly cacheStorage: CacheStorageService,
private readonly twentyORMManager: TwentyORMManager, private readonly twentyORMManager: TwentyORMManager,
@InjectObjectMetadataRepository(BlocklistWorkspaceEntity) @InjectObjectMetadataRepository(BlocklistWorkspaceEntity)
private readonly blocklistRepository: BlocklistRepository, private readonly blocklistRepository: BlocklistRepository,
private readonly calendarEventCleanerService: CalendarEventCleanerService, private readonly calendarEventCleanerService: CalendarEventCleanerService,
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
private readonly getCalendarEventsService: CalendarGetCalendarEventsService,
private readonly calendarSaveEventsService: CalendarSaveEventsService, private readonly calendarSaveEventsService: CalendarSaveEventsService,
private readonly calendarEventImportErrorHandlerService: CalendarEventImportErrorHandlerService, private readonly calendarEventImportErrorHandlerService: CalendarEventImportErrorHandlerService,
private readonly microsoftCalendarImportEventService: MicrosoftCalendarImportEventsService,
) {} ) {}
public async processCalendarEventsImport( public async processCalendarEventsImport(
calendarChannel: CalendarChannelWorkspaceEntity, calendarChannel: CalendarChannelWorkspaceEntity,
connectedAccount: ConnectedAccountWorkspaceEntity, connectedAccount: ConnectedAccountWorkspaceEntity,
workspaceId: string, workspaceId: string,
fetchedCalendarEvents?: CalendarEventWithParticipants[],
): Promise<void> { ): Promise<void> {
const syncStep = await this.calendarChannelSyncStatusService.markAsCalendarEventsImportOngoing(
calendarChannel.syncStage ===
CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING
? CalendarEventImportSyncStep.FULL_CALENDAR_EVENT_LIST_FETCH
: CalendarEventImportSyncStep.PARTIAL_CALENDAR_EVENT_LIST_FETCH;
await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchOngoing(
[calendarChannel.id], [calendarChannel.id],
); );
let calendarEvents: GetCalendarEventsResponse['calendarEvents'] = [];
let nextSyncCursor: GetCalendarEventsResponse['nextSyncCursor'] = ''; let calendarEvents: CalendarEventWithParticipants[] = [];
try { try {
const getCalendarEventsResponse = if (fetchedCalendarEvents) {
await this.getCalendarEventsService.getCalendarEvents( calendarEvents = fetchedCalendarEvents;
} else {
const eventIdsToFetch: string[] = await this.cacheStorage.setPop(
`calendar-events-to-import:${workspaceId}:${calendarChannel.id}`,
CALENDAR_EVENT_IMPORT_BATCH_SIZE,
);
if (!eventIdsToFetch || eventIdsToFetch.length === 0) {
await this.calendarChannelSyncStatusService.markAsCompletedAndSchedulePartialCalendarEventListFetch(
[calendarChannel.id],
);
return;
}
switch (connectedAccount.provider) {
case 'microsoft':
calendarEvents =
await this.microsoftCalendarImportEventService.getCalendarEvents(
connectedAccount, connectedAccount,
calendarChannel.syncCursor, eventIdsToFetch,
);
calendarEvents = getCalendarEventsResponse.calendarEvents;
nextSyncCursor = getCalendarEventsResponse.nextSyncCursor;
const calendarChannelRepository =
await this.twentyORMManager.getRepository<CalendarChannelWorkspaceEntity>(
'calendarChannel',
); );
break;
default:
break;
}
}
if (!calendarEvents || calendarEvents?.length === 0) { if (!calendarEvents || calendarEvents?.length === 0) {
await calendarChannelRepository.update(
{
id: calendarChannel.id,
},
{
syncCursor: nextSyncCursor,
},
);
await this.calendarChannelSyncStatusService.schedulePartialCalendarEventListFetch( await this.calendarChannelSyncStatusService.schedulePartialCalendarEventListFetch(
[calendarChannel.id], [calendarChannel.id],
); );
@ -127,22 +129,13 @@ export class CalendarEventsImportService {
workspaceId, workspaceId,
); );
await calendarChannelRepository.update(
{
id: calendarChannel.id,
},
{
syncCursor: nextSyncCursor,
},
);
await this.calendarChannelSyncStatusService.markAsCompletedAndSchedulePartialCalendarEventListFetch( await this.calendarChannelSyncStatusService.markAsCompletedAndSchedulePartialCalendarEventListFetch(
[calendarChannel.id], [calendarChannel.id],
); );
} catch (error) { } catch (error) {
await this.calendarEventImportErrorHandlerService.handleDriverException( await this.calendarEventImportErrorHandlerService.handleDriverException(
error, error,
syncStep, CalendarEventImportSyncStep.CALENDAR_EVENTS_IMPORT,
calendarChannel, calendarChannel,
workspaceId, workspaceId,
); );

View File

@ -0,0 +1,129 @@
import { Injectable } from '@nestjs/common';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import {
CalendarEventImportDriverException,
CalendarEventImportDriverExceptionCode,
} from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception';
import {
CalendarEventImportErrorHandlerService,
CalendarEventImportSyncStep,
} from 'src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service';
import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service';
import { CalendarGetCalendarEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import {
CalendarChannelSyncStage,
CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Injectable()
export class CalendarFetchEventsService {
constructor(
@InjectCacheStorage(CacheStorageNamespace.ModuleCalendar)
private readonly cacheStorage: CacheStorageService,
private readonly twentyORMManager: TwentyORMManager,
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
private readonly getCalendarEventsService: CalendarGetCalendarEventsService,
private readonly calendarEventImportErrorHandlerService: CalendarEventImportErrorHandlerService,
private readonly calendarEventsImportService: CalendarEventsImportService,
) {}
public async fetchCalendarEvents(
calendarChannel: CalendarChannelWorkspaceEntity,
connectedAccount: ConnectedAccountWorkspaceEntity,
workspaceId: string,
): Promise<void> {
const syncStep =
calendarChannel.syncStage ===
CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING
? CalendarEventImportSyncStep.FULL_CALENDAR_EVENT_LIST_FETCH
: CalendarEventImportSyncStep.PARTIAL_CALENDAR_EVENT_LIST_FETCH;
await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchOngoing(
[calendarChannel.id],
);
try {
const getCalendarEventsResponse =
await this.getCalendarEventsService.getCalendarEvents(
connectedAccount,
calendarChannel.syncCursor,
);
const hasFullEvents = getCalendarEventsResponse.fullEvents;
const calendarEvents = hasFullEvents
? getCalendarEventsResponse.calendarEvents
: null;
const calendarEventIds = getCalendarEventsResponse.calendarEventIds;
const nextSyncCursor = getCalendarEventsResponse.nextSyncCursor;
const calendarChannelRepository =
await this.twentyORMManager.getRepository<CalendarChannelWorkspaceEntity>(
'calendarChannel',
);
if (!calendarEvents || calendarEvents?.length === 0) {
await calendarChannelRepository.update(
{
id: calendarChannel.id,
},
{
syncCursor: nextSyncCursor,
},
);
await this.calendarChannelSyncStatusService.schedulePartialCalendarEventListFetch(
[calendarChannel.id],
);
}
await calendarChannelRepository.update(
{
id: calendarChannel.id,
},
{
syncCursor: nextSyncCursor,
},
);
if (hasFullEvents && calendarEvents) {
// Event Import already done
await this.calendarEventsImportService.processCalendarEventsImport(
calendarChannel,
connectedAccount,
workspaceId,
calendarEvents,
);
} else if (!hasFullEvents && calendarEventIds) {
// Event Import still needed
await this.cacheStorage.setAdd(
`calendar-events-to-import:${workspaceId}:${calendarChannel.id}`,
calendarEventIds,
);
await this.calendarChannelSyncStatusService.scheduleCalendarEventsImport(
[calendarChannel.id],
);
} else {
throw new CalendarEventImportDriverException(
"Expected 'calendarEvents' or 'calendarEventIds' to be present",
CalendarEventImportDriverExceptionCode.UNKNOWN,
);
}
} catch (error) {
await this.calendarEventImportErrorHandlerService.handleDriverException(
error,
syncStep,
calendarChannel,
workspaceId,
);
}
}
}

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { GoogleCalendarGetEventsService as GoogleCalendarGetCalendarEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service'; import { GoogleCalendarGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service';
import { MicrosoftCalendarGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-get-events.service';
import { import {
CalendarEventImportException, CalendarEventImportException,
CalendarEventImportExceptionCode, CalendarEventImportExceptionCode,
@ -9,14 +10,17 @@ import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type GetCalendarEventsResponse = { export type GetCalendarEventsResponse = {
calendarEvents: CalendarEventWithParticipants[]; fullEvents: boolean;
calendarEvents?: CalendarEventWithParticipants[];
calendarEventIds?: string[];
nextSyncCursor: string; nextSyncCursor: string;
}; };
@Injectable() @Injectable()
export class CalendarGetCalendarEventsService { export class CalendarGetCalendarEventsService {
constructor( constructor(
private readonly googleCalendarGetCalendarEventsService: GoogleCalendarGetCalendarEventsService, private readonly googleCalendarGetEventsService: GoogleCalendarGetEventsService,
private readonly microsoftCalendarGetEventsService: MicrosoftCalendarGetEventsService,
) {} ) {}
public async getCalendarEvents( public async getCalendarEvents(
@ -28,7 +32,12 @@ export class CalendarGetCalendarEventsService {
): Promise<GetCalendarEventsResponse> { ): Promise<GetCalendarEventsResponse> {
switch (connectedAccount.provider) { switch (connectedAccount.provider) {
case 'google': case 'google':
return this.googleCalendarGetCalendarEventsService.getCalendarEvents( return this.googleCalendarGetEventsService.getCalendarEvents(
connectedAccount,
syncCursor,
);
case 'microsoft':
return this.microsoftCalendarGetEventsService.getCalendarEvents(
connectedAccount, connectedAccount,
syncCursor, syncCursor,
); );

View File

@ -23,7 +23,7 @@ export const filterEventsAndReturnCancelledEvents = (
}, },
event, event,
) => { ) => {
if (event.status === 'cancelled') { if (event.isCanceled) {
acc.cancelledEvents.push(event); acc.cancelledEvents.push(event);
} else { } else {
acc.filteredEvents.push(event); acc.filteredEvents.push(event);

View File

@ -0,0 +1,62 @@
import { Injectable } from '@nestjs/common';
import {
AuthProvider,
AuthProviderCallback,
Client,
} from '@microsoft/microsoft-graph-client';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Injectable()
export class MicrosoftOAuth2ClientManagerService {
constructor(private readonly environmentService: EnvironmentService) {}
public async getOAuth2Client(refreshToken: string): Promise<Client> {
const authProvider: AuthProvider = async (
callback: AuthProviderCallback,
) => {
try {
const tenantId = this.environmentService.get(
'AUTH_MICROSOFT_TENANT_ID',
);
const urlData = new URLSearchParams();
urlData.append(
'client_id',
this.environmentService.get('AUTH_MICROSOFT_CLIENT_ID'),
);
urlData.append('scope', 'https://graph.microsoft.com/.default');
urlData.append('refresh_token', refreshToken);
urlData.append(
'client_secret',
this.environmentService.get('AUTH_MICROSOFT_CLIENT_SECRET'),
);
urlData.append('grant_type', 'refresh_token');
const res = await fetch(
`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
{
method: 'POST',
body: urlData,
},
);
const data = await res.json();
callback(null, data.access_token);
} catch (error) {
callback(error, null);
}
};
const client = Client.init({
defaultVersion: 'v1.0',
debugLogging: false,
authProvider: authProvider,
});
return client;
}
}

View File

@ -1,11 +1,16 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { GoogleOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service'; import { GoogleOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
@Module({ @Module({
imports: [], imports: [],
providers: [OAuth2ClientManagerService, GoogleOAuth2ClientManagerService], providers: [
exports: [OAuth2ClientManagerService], OAuth2ClientManagerService,
GoogleOAuth2ClientManagerService,
MicrosoftOAuth2ClientManagerService,
],
exports: [OAuth2ClientManagerService, MicrosoftOAuth2ClientManagerService],
}) })
export class OAuth2ClientManagerModule {} export class OAuth2ClientManagerModule {}

View File

@ -22,6 +22,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
export enum ConnectedAccountProvider { export enum ConnectedAccountProvider {
GOOGLE = 'google', GOOGLE = 'google',
MICROSOFT = 'microsoft',
} }
@WorkspaceEntity({ @WorkspaceEntity({

View File

@ -35,6 +35,12 @@ export class MessagingGetMessageListService {
return this.gmailGetMessageListService.getFullMessageList( return this.gmailGetMessageListService.getFullMessageList(
connectedAccount, connectedAccount,
); );
case 'microsoft':
// TODO: Placeholder
return {
messageExternalIds: [],
nextSyncCursor: '',
};
default: default:
throw new MessageImportException( throw new MessageImportException(
`Provider ${connectedAccount.provider} is not supported`, `Provider ${connectedAccount.provider} is not supported`,
@ -56,6 +62,12 @@ export class MessagingGetMessageListService {
connectedAccount, connectedAccount,
syncCursor, syncCursor,
); );
case 'microsoft':
return {
messageExternalIds: [],
messageExternalIdsToDelete: [],
nextSyncCursor: '',
};
default: default:
throw new MessageImportException( throw new MessageImportException(
`Provider ${connectedAccount.provider} is not supported`, `Provider ${connectedAccount.provider} is not supported`,

View File

@ -75,6 +75,7 @@ yarn command:prod cron:calendar:calendar-event-list-fetch
['AUTH_MICROSOFT_TENANT_ID', '', 'Microsoft tenant ID'], ['AUTH_MICROSOFT_TENANT_ID', '', 'Microsoft tenant ID'],
['AUTH_MICROSOFT_CLIENT_SECRET', '', 'Microsoft client secret'], ['AUTH_MICROSOFT_CLIENT_SECRET', '', 'Microsoft client secret'],
['AUTH_MICROSOFT_CALLBACK_URL', 'http://[YourDomain]/auth/microsoft/redirect', 'Microsoft auth callback'], ['AUTH_MICROSOFT_CALLBACK_URL', 'http://[YourDomain]/auth/microsoft/redirect', 'Microsoft auth callback'],
['AUTH_GOOGLE_APIS_CALLBACK_URL', 'http://[YourDomain]/auth/microsoft-apis/get-access-token', 'Microsoft APIs auth callback'],
['FRONT_AUTH_CALLBACK_URL', 'http://localhost:3001/verify ', 'Callback used for Login page'], ['FRONT_AUTH_CALLBACK_URL', 'http://localhost:3001/verify ', 'Callback used for Login page'],
['IS_SIGN_UP_DISABLED', 'false', 'Disable sign-up'], ['IS_SIGN_UP_DISABLED', 'false', 'Disable sign-up'],
['PASSWORD_RESET_TOKEN_EXPIRES_IN', '5m', 'Password reset token expiration time'], ['PASSWORD_RESET_TOKEN_EXPIRES_IN', '5m', 'Password reset token expiration time'],

View File

@ -6682,6 +6682,32 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@microsoft/microsoft-graph-client@npm:^3.0.7":
version: 3.0.7
resolution: "@microsoft/microsoft-graph-client@npm:3.0.7"
dependencies:
"@babel/runtime": "npm:^7.12.5"
tslib: "npm:^2.2.0"
peerDependenciesMeta:
"@azure/identity":
optional: true
"@azure/msal-browser":
optional: true
buffer:
optional: true
stream-browserify:
optional: true
checksum: 10c0/0e5b3dd469be0606ba34a82464d18a8c919f396adf65756612e9ec0bb47c99c4c3eaf5d65eec953e2c6f5b79e48e003efe914adbfc8b741d098e8e8f7084ea8b
languageName: node
linkType: hard
"@microsoft/microsoft-graph-types@npm:^2.40.0":
version: 2.40.0
resolution: "@microsoft/microsoft-graph-types@npm:2.40.0"
checksum: 10c0/c6f69a0fe136579d735efafc1375e4540e01d34ea488d987a9b651f7206c40c76991fff56df31e8b2593f16846bd748d0ffcf249d1f2629bb0298e6214fe2fdc
languageName: node
linkType: hard
"@microsoft/tsdoc-config@npm:~0.16.1": "@microsoft/tsdoc-config@npm:~0.16.1":
version: 0.16.2 version: 0.16.2
resolution: "@microsoft/tsdoc-config@npm:0.16.2" resolution: "@microsoft/tsdoc-config@npm:0.16.2"
@ -44331,6 +44357,8 @@ __metadata:
"@linaria/core": "npm:^6.2.0" "@linaria/core": "npm:^6.2.0"
"@linaria/react": "npm:^6.2.1" "@linaria/react": "npm:^6.2.1"
"@mdx-js/react": "npm:^3.0.0" "@mdx-js/react": "npm:^3.0.0"
"@microsoft/microsoft-graph-client": "npm:^3.0.7"
"@microsoft/microsoft-graph-types": "npm:^2.40.0"
"@nestjs/apollo": "npm:^11.0.5" "@nestjs/apollo": "npm:^11.0.5"
"@nestjs/axios": "npm:^3.0.1" "@nestjs/axios": "npm:^3.0.1"
"@nestjs/cli": "npm:^9.0.0" "@nestjs/cli": "npm:^9.0.0"